From 71b846a4ef80ad7850a9095ab4cba2fa302ac522 Mon Sep 17 00:00:00 2001 From: Syed Sultan Beevi Date: Thu, 28 May 2026 13:33:15 +0530 Subject: [PATCH 1/3] feat(01-learn): add 18-input-output-guardrails tutorial --- .../01_input_guardrail.ipynb | 464 ++++++++++++++++ .../02_output_guardrail.ipynb | 452 ++++++++++++++++ .../03_content_filters.ipynb | 426 +++++++++++++++ .../04_guardrail_plugin.ipynb | 502 +++++++++++++++++ .../05_tool_call_validation.ipynb | 506 ++++++++++++++++++ .../06_error_handling.ipynb | 468 ++++++++++++++++ .../18-input-output-guardrails/README.md | 442 +++++++++++++++ .../images/filter_pipeline.png | Bin 0 -> 54720 bytes .../images/guardrail_architecture.png | Bin 0 -> 62219 bytes .../images/plugin_architecture.png | Bin 0 -> 111958 bytes .../requirements.txt | 4 + 11 files changed, 3264 insertions(+) create mode 100644 python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb create mode 100644 python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb create mode 100644 python/01-learn/18-input-output-guardrails/03_content_filters.ipynb create mode 100644 python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb create mode 100644 python/01-learn/18-input-output-guardrails/05_tool_call_validation.ipynb create mode 100644 python/01-learn/18-input-output-guardrails/06_error_handling.ipynb create mode 100644 python/01-learn/18-input-output-guardrails/README.md create mode 100644 python/01-learn/18-input-output-guardrails/images/filter_pipeline.png create mode 100644 python/01-learn/18-input-output-guardrails/images/guardrail_architecture.png create mode 100644 python/01-learn/18-input-output-guardrails/images/plugin_architecture.png create mode 100644 python/01-learn/18-input-output-guardrails/requirements.txt diff --git a/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb b/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb new file mode 100644 index 00000000..581b6367 --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb @@ -0,0 +1,464 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Input Guardrails\n", + "\n", + "This notebook demonstrates how to implement **input guardrails** using the Strands Agents SDK's `BeforeInvocationEvent` hook.\n", + "\n", + "Input guardrails inspect user messages **before** they reach the model, allowing you to:\n", + "- Block harmful, off-topic, or non-compliant requests\n", + "- Detect and reject PII (emails, phone numbers, SSNs)\n", + "- Compose multiple content filters for layered protection\n", + "\n", + "**Key concept:** Use a `HookProvider` class with `BeforeInvocationEvent` to intercept messages before model inference. Access messages via `event.agent.messages`. When a violation is detected, replace the messages with a rejection prompt.\n", + "\n", + "## Architecture\n", + "\n", + "The following diagram shows where input guardrails sit in the agent lifecycle:\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Make sure you have the required packages installed and the content filters module (`content_filters.py`) available in the same directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "!pip install strands-agents strands-agents-tools --upgrade -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent\n", + "\n", + "# Import content filters from our shared module\n", + "import content_filters as _filters_module\n", + "KeywordContentFilter = _filters_module.KeywordContentFilter\n", + "RegexContentFilter = _filters_module.RegexContentFilter\n", + "Severity = _filters_module.Severity\n", + "run_filters = _filters_module.run_filters\n", + "\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper: Extract Text from a Message\n", + "\n", + "Messages follow the Bedrock Converse API format:\n", + "```python\n", + "{\"role\": \"user\", \"content\": [{\"text\": \"user message here\"}]}\n", + "```\n", + "\n", + "This helper extracts and concatenates all text content blocks from a message." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _extract_text_from_message(message: dict) -> str:\n", + " \"\"\"Extract text content from a Strands SDK message.\n", + "\n", + " Args:\n", + " message: A message dictionary with 'role' and 'content' keys.\n", + "\n", + " Returns:\n", + " Concatenated text from all text content blocks in the message.\n", + " \"\"\"\n", + " text_parts = []\n", + " for block in message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " text_parts.append(block[\"text\"])\n", + " return \" \".join(text_parts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Input Guardrail: Keyword-Based Content Blocking\n", + "\n", + "The simplest guardrail pattern: define a list of prohibited keywords and block any request that contains them. This uses the `KeywordContentFilter` from our shared content filters module.\n", + "\n", + "We define the guardrail logic as a standalone function first (for easy testing with mocks), then wrap it in a `HookProvider` class for agent registration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define prohibited keywords for topic blocking\n", + "PROHIBITED_KEYWORDS = [\"hack\", \"exploit\", \"bypass security\", \"illegal\", \"steal credentials\"]\n", + "\n", + "# Create a keyword filter instance\n", + "keyword_filter = KeywordContentFilter(\n", + " name=\"prohibited_topics\",\n", + " keywords=PROHIBITED_KEYWORDS,\n", + " severity=Severity.BLOCK,\n", + ")\n", + "\n", + "\n", + "def input_guardrail_logic(messages: list) -> None:\n", + " \"\"\"Simple input guardrail that blocks prohibited topics.\n", + "\n", + " Inspects the last user message and blocks requests containing\n", + " prohibited keywords by replacing messages with a rejection prompt.\n", + " \"\"\"\n", + " if not messages:\n", + " return\n", + "\n", + " last_message = messages[-1]\n", + " if last_message.get(\"role\") != \"user\":\n", + " return\n", + "\n", + " text = _extract_text_from_message(last_message)\n", + " if not text:\n", + " return\n", + "\n", + " result = keyword_filter.evaluate(text)\n", + "\n", + " if not result.passed:\n", + " logger.warning(\n", + " f\"[INPUT GUARDRAIL] Blocked request. \"\n", + " f\"Filter: {result.filter_name}, Reason: {result.message}\"\n", + " )\n", + " messages.clear()\n", + " messages.append(\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " \"text\": (\n", + " \"Respond only with: I cannot process that request. \"\n", + " \"The input was blocked by a content safety filter.\"\n", + " )\n", + " }\n", + " ],\n", + " }\n", + " )\n", + " else:\n", + " logger.debug(\"[INPUT GUARDRAIL] Request passed keyword filter.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Input Guardrail: PII Detection\n", + "\n", + "This guardrail detects personally identifiable information (emails, phone numbers, SSNs) in user messages and blocks the request to prevent PII from being sent to the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define PII patterns\n", + "PII_PATTERNS = [\n", + " r\"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\", # Email\n", + " r\"\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b\", # Phone number\n", + " r\"\\b\\d{3}-\\d{2}-\\d{4}\\b\", # SSN\n", + "]\n", + "\n", + "# Create a PII filter instance that blocks requests containing PII\n", + "pii_filter = RegexContentFilter(\n", + " name=\"pii_detector\",\n", + " patterns=PII_PATTERNS,\n", + " severity=Severity.BLOCK,\n", + ")\n", + "\n", + "\n", + "def pii_input_guardrail_logic(messages: list) -> None:\n", + " \"\"\"Input guardrail that blocks requests containing PII.\"\"\"\n", + " if not messages:\n", + " return\n", + "\n", + " last_message = messages[-1]\n", + " if last_message.get(\"role\") != \"user\":\n", + " return\n", + "\n", + " text = _extract_text_from_message(last_message)\n", + " if not text:\n", + " return\n", + "\n", + " result = pii_filter.evaluate(text)\n", + "\n", + " if not result.passed:\n", + " logger.warning(\n", + " f\"[PII GUARDRAIL] Blocked request containing PII. \"\n", + " f\"Filter: {result.filter_name}, Reason: {result.message}\"\n", + " )\n", + " messages.clear()\n", + " messages.append(\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " \"text\": (\n", + " \"Respond only with: I cannot process that request. \"\n", + " \"Personal information (PII) was detected in your message. \"\n", + " \"Please remove any email addresses, phone numbers, or \"\n", + " \"social security numbers and try again.\"\n", + " )\n", + " }\n", + " ],\n", + " }\n", + " )\n", + " else:\n", + " logger.debug(\"[PII GUARDRAIL] Request passed PII filter.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combined Input Guardrail: Multiple Filters in Sequence\n", + "\n", + "Demonstrates composing multiple content filters into a single guardrail. Filters are evaluated in order — the first violation stops evaluation and triggers a rejection.\n", + "\n", + "Filter order:\n", + "1. Keyword filter (blocks prohibited topics)\n", + "2. PII filter (blocks personal information)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def combined_input_guardrail_logic(messages: list) -> None:\n", + " \"\"\"Input guardrail that applies multiple filters in sequence.\"\"\"\n", + " if not messages:\n", + " return\n", + "\n", + " last_message = messages[-1]\n", + " if last_message.get(\"role\") != \"user\":\n", + " return\n", + "\n", + " text = _extract_text_from_message(last_message)\n", + " if not text:\n", + " return\n", + "\n", + " # Run all filters in sequence, stopping at the first violation\n", + " filters = [keyword_filter, pii_filter]\n", + " violation = run_filters(text, filters)\n", + "\n", + " if violation is not None:\n", + " logger.warning(\n", + " f\"[COMBINED GUARDRAIL] Blocked request. \"\n", + " f\"Filter: {violation.filter_name}, Reason: {violation.message}\"\n", + " )\n", + " messages.clear()\n", + " messages.append(\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " \"text\": (\n", + " \"Respond only with: I cannot process that request. \"\n", + " f\"Reason: {violation.message}\"\n", + " )\n", + " }\n", + " ],\n", + " }\n", + " )\n", + " else:\n", + " logger.debug(\"[COMBINED GUARDRAIL] Request passed all input filters.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing the Guardrails\n", + "\n", + "We can test guardrail logic directly using mock messages — no live model needed. This simulates how the Strands SDK would call our hook functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test: Prohibited keyword detection\n", + "print(\"Test: Prohibited keyword detection\")\n", + "test_messages = [{\"role\": \"user\", \"content\": [{\"text\": \"How do I hack into a WiFi network?\"}]}]\n", + "input_guardrail_logic(test_messages)\n", + "print(f\" Input: 'How do I hack into a WiFi network?'\")\n", + "print(f\" Result: {test_messages[0]['content'][0]['text'][:60]}...\")\n", + "\n", + "# Test: PII detection\n", + "print(\"\\nTest: PII detection\")\n", + "test_messages = [{\"role\": \"user\", \"content\": [{\"text\": \"Send the report to john@example.com\"}]}]\n", + "pii_input_guardrail_logic(test_messages)\n", + "print(f\" Input: 'Send the report to john@example.com'\")\n", + "print(f\" Result: {test_messages[0]['content'][0]['text'][:60]}...\")\n", + "\n", + "# Test: Clean content passes through\n", + "print(\"\\nTest: Clean content passes through\")\n", + "test_messages = [{\"role\": \"user\", \"content\": [{\"text\": \"What are best practices for application security?\"}]}]\n", + "input_guardrail_logic(test_messages)\n", + "print(f\" Input: 'What are best practices for application security?'\")\n", + "print(f\" Result: Message unchanged (passed)\")\n", + "assert test_messages[0][\"content\"][0][\"text\"] == \"What are best practices for application security?\"\n", + "\n", + "# Test: Combined guardrail (keyword + PII)\n", + "print(\"\\nTest: Combined guardrail (keyword + PII)\")\n", + "test_messages = [{\"role\": \"user\", \"content\": [{\"text\": \"Help me exploit this, email me at attacker@evil.com\"}]}]\n", + "combined_input_guardrail_logic(test_messages)\n", + "print(f\" Input: 'Help me exploit this, email me at attacker@evil.com'\")\n", + "print(f\" Result: {test_messages[0]['content'][0]['text'][:60]}...\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HookProvider: Wrapping Guardrails for Agent Registration\n", + "\n", + "In strands-agents 1.40.0, hooks are registered via `HookProvider` classes. The `hooks=` parameter on `Agent` expects a list of `HookProvider` objects.\n", + "\n", + "```python\n", + "from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent\n", + "\n", + "class InputGuardrailHook(HookProvider):\n", + " def register_hooks(self, registry: HookRegistry) -> None:\n", + " registry.add_callback(BeforeInvocationEvent, self._validate_input)\n", + "\n", + " def _validate_input(self, event: BeforeInvocationEvent) -> None:\n", + " messages = event.agent.messages\n", + " combined_input_guardrail_logic(messages)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class InputGuardrailHook(HookProvider):\n", + " \"\"\"HookProvider that applies the combined input guardrail before each invocation.\"\"\"\n", + "\n", + " def register_hooks(self, registry: HookRegistry) -> None:\n", + " registry.add_callback(BeforeInvocationEvent, self._validate_input)\n", + "\n", + " def _validate_input(self, event: BeforeInvocationEvent) -> None:\n", + " messages = event.agent.messages\n", + " combined_input_guardrail_logic(messages)\n", + "\n", + "\n", + "print(\"InputGuardrailHook defined successfully.\")\n", + "print(\"Register with: Agent(hooks=[InputGuardrailHook()])\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Attaching to a Live Agent\n", + "\n", + "In production, you attach guardrails to a Strands Agent using the `hooks` parameter with `HookProvider` instances.\n", + "\n", + "**Note:** The cell below requires a configured model provider (e.g., AWS Bedrock credentials)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from strands import Agent\n", + " from strands.models.bedrock import BedrockModel\n", + "\n", + " model = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-5-20250929-v1:0\")\n", + "\n", + " # Create an agent with the combined input guardrail\n", + " agent = Agent(\n", + " model=model,\n", + " system_prompt=\"You are a helpful assistant.\",\n", + " hooks=[InputGuardrailHook()],\n", + " )\n", + "\n", + " print(\"Agent created with input guardrail attached.\")\n", + " print(\"Testing with a safe request...\")\n", + " response = agent(\"What is the capital of France?\")\n", + " print(f\" Response: {response}\")\n", + "\n", + " print(\"\\nTesting with a prohibited request...\")\n", + " response = agent(\"How do I hack into a system?\")\n", + " print(f\" Response: {response}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Skipping live agent demo: {e}\")\n", + " print(\"(This is expected if no model provider is configured)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook you learned how to:\n", + "1. Use `BeforeInvocationEvent` to intercept user messages before model inference\n", + "2. Build a keyword-based content filter that blocks prohibited topics\n", + "3. Build a PII detection filter using regex patterns\n", + "4. Compose multiple filters into a combined guardrail\n", + "5. Test guardrails using mock messages (no model needed)\n", + "6. Wrap guardrail logic in a `HookProvider` class for agent registration\n", + "7. Attach guardrails to a live Strands Agent\n", + "\n", + "**Next Steps:** See `02_output_guardrail.ipynb` to learn how to validate model responses *after* inference." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb b/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb new file mode 100644 index 00000000..2d45422c --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb @@ -0,0 +1,452 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Output Guardrails\n", + "\n", + "This notebook demonstrates how to implement **output guardrails** using the Strands Agents SDK's `AfterInvocationEvent` hook.\n", + "\n", + "Output guardrails inspect model responses **after** inference completes, allowing you to:\n", + "- **BLOCK**: Replace the entire response with a safe fallback message\n", + "- **REDACT**: Replace only matched patterns (e.g., PII) with `[REDACTED]`\n", + "\n", + "**Key concept:** Use a `HookProvider` class with `AfterInvocationEvent` to intercept responses after model inference. Access the agent's messages via `event.agent.messages` and modify the last assistant message." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "!pip install strands-agents strands-agents-tools --upgrade -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "from strands.hooks import HookProvider, HookRegistry, AfterInvocationEvent\n", + "\n", + "# Import content filters from our shared module\n", + "import content_filters as _filters_module\n", + "KeywordContentFilter = _filters_module.KeywordContentFilter\n", + "RegexContentFilter = _filters_module.RegexContentFilter\n", + "FormatComplianceFilter = _filters_module.FormatComplianceFilter\n", + "Severity = _filters_module.Severity\n", + "FilterResult = _filters_module.FilterResult\n", + "run_filters = _filters_module.run_filters\n", + "\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper: Modify the Last Assistant Message\n", + "\n", + "Output guardrails work by modifying the conversation history. These helpers replace or redact the last assistant message content." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _replace_assistant_response(messages: list, new_text: str) -> None:\n", + " \"\"\"Replace the last assistant message content with new text.\"\"\"\n", + " if not messages:\n", + " return\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " message[\"content\"] = [{\"text\": new_text}]\n", + " return\n", + "\n", + "\n", + "def _redact_assistant_response(messages: list, redacted_text: str) -> None:\n", + " \"\"\"Redact specific patterns in the last assistant message.\n", + " \n", + " Unlike full replacement, redaction preserves the overall response structure\n", + " but replaces sensitive patterns with [REDACTED].\n", + " \"\"\"\n", + " if not messages:\n", + " return\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " new_content = []\n", + " for block in message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " new_content.append({\"text\": redacted_text})\n", + " else:\n", + " new_content.append(block)\n", + " message[\"content\"] = new_content\n", + " return" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output Guardrail: BLOCK Behavior\n", + "\n", + "The BLOCK behavior replaces the **entire** response with a safe fallback message when prohibited content is detected in the model's output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define prohibited keywords that should never appear in model output\n", + "OUTPUT_PROHIBITED_KEYWORDS = [\n", + " \"confidential internal\",\n", + " \"classified information\",\n", + " \"trade secret\",\n", + " \"proprietary algorithm\",\n", + "]\n", + "\n", + "output_keyword_filter = KeywordContentFilter(\n", + " name=\"output_topic_blocker\",\n", + " keywords=OUTPUT_PROHIBITED_KEYWORDS,\n", + " severity=Severity.BLOCK,\n", + ")\n", + "\n", + "BLOCKED_RESPONSE_FALLBACK = (\n", + " \"I'm sorry, but I cannot provide that information. \"\n", + " \"The response was blocked by a content safety filter.\"\n", + ")\n", + "\n", + "\n", + "def output_guardrail_logic(messages: list) -> None:\n", + " \"\"\"Output guardrail that blocks responses containing prohibited content.\"\"\"\n", + " if not messages:\n", + " return\n", + "\n", + " # Find the last assistant message\n", + " last_assistant_text = None\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " for block in message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " last_assistant_text = block[\"text\"]\n", + " break\n", + " break\n", + "\n", + " if not last_assistant_text:\n", + " return\n", + "\n", + " result = output_keyword_filter.evaluate(last_assistant_text)\n", + "\n", + " if not result.passed:\n", + " logger.warning(\n", + " f\"[OUTPUT GUARDRAIL] Blocked response. \"\n", + " f\"Filter: {result.filter_name}, Reason: {result.message}\"\n", + " )\n", + " _replace_assistant_response(messages, BLOCKED_RESPONSE_FALLBACK)\n", + " else:\n", + " logger.debug(\"[OUTPUT GUARDRAIL] Response passed keyword filter.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output Guardrail: REDACT Behavior\n", + "\n", + "The REDACT behavior replaces only the **matched patterns** (like PII) with `[REDACTED]`, preserving the rest of the response. This is less disruptive than blocking the entire response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define PII patterns for output redaction\n", + "OUTPUT_PII_PATTERNS = [\n", + " r\"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\", # Email\n", + " r\"\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b\", # Phone number\n", + " r\"\\b\\d{3}-\\d{2}-\\d{4}\\b\", # SSN\n", + "]\n", + "\n", + "pii_redaction_filter = RegexContentFilter(\n", + " name=\"output_pii_redactor\",\n", + " patterns=OUTPUT_PII_PATTERNS,\n", + " severity=Severity.REDACT,\n", + ")\n", + "\n", + "\n", + "def pii_output_guardrail_logic(messages: list) -> None:\n", + " \"\"\"Output guardrail that redacts PII from model responses.\"\"\"\n", + " if not messages:\n", + " return\n", + "\n", + " # Find the last assistant message text\n", + " last_assistant_text = None\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " for block in message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " last_assistant_text = block[\"text\"]\n", + " break\n", + " break\n", + "\n", + " if not last_assistant_text:\n", + " return\n", + "\n", + " result = pii_redaction_filter.evaluate(last_assistant_text)\n", + "\n", + " if not result.passed:\n", + " logger.warning(\n", + " f\"[PII OUTPUT GUARDRAIL] Redacted PII from response. \"\n", + " f\"Filter: {result.filter_name}, Reason: {result.message}\"\n", + " )\n", + " _redact_assistant_response(messages, result.redacted_text)\n", + " else:\n", + " logger.debug(\"[PII OUTPUT GUARDRAIL] Response passed PII filter.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combined Output Guardrail: Mixed Behaviors\n", + "\n", + "Compose multiple output filters with different behaviors:\n", + "1. **Keyword filter (BLOCK)** — replaces entire response if triggered\n", + "2. **Format compliance filter (BLOCK)** — blocks code execution instructions\n", + "3. **PII filter (REDACT)** — redacts matched patterns only\n", + "\n", + "BLOCK-severity violations are checked first. If none are found, REDACT filters clean up the response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "format_filter = FormatComplianceFilter(\n", + " name=\"output_format_compliance\",\n", + " severity=Severity.BLOCK,\n", + ")\n", + "\n", + "\n", + "def combined_output_guardrail_logic(messages: list) -> None:\n", + " \"\"\"Output guardrail that applies multiple filters with different behaviors.\"\"\"\n", + " if not messages:\n", + " return\n", + "\n", + " # Find the last assistant message text\n", + " response_text = None\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " for block in message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " response_text = block[\"text\"]\n", + " break\n", + " break\n", + "\n", + " if not response_text:\n", + " return\n", + "\n", + " # Phase 1: Check BLOCK-severity filters first\n", + " block_filters = [output_keyword_filter, format_filter]\n", + " violation = run_filters(response_text, block_filters)\n", + "\n", + " if violation is not None:\n", + " logger.warning(\n", + " f\"[COMBINED OUTPUT GUARDRAIL] Blocked response. \"\n", + " f\"Filter: {violation.filter_name}, Reason: {violation.message}\"\n", + " )\n", + " _replace_assistant_response(messages, BLOCKED_RESPONSE_FALLBACK)\n", + " return\n", + "\n", + " # Phase 2: Apply REDACT-severity filters (PII redaction)\n", + " redact_result = pii_redaction_filter.evaluate(response_text)\n", + "\n", + " if not redact_result.passed:\n", + " logger.info(\n", + " f\"[COMBINED OUTPUT GUARDRAIL] Redacted content from response. \"\n", + " f\"Filter: {redact_result.filter_name}, Reason: {redact_result.message}\"\n", + " )\n", + " _redact_assistant_response(messages, redact_result.redacted_text)\n", + " else:\n", + " logger.debug(\"[COMBINED OUTPUT GUARDRAIL] Response passed all output filters.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing the Output Guardrails\n", + "\n", + "We test output guardrails using mock messages that simulate the conversation state after model inference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test BLOCK behavior\n", + "print(\"Test: BLOCK — Prohibited keyword in output\")\n", + "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Here is the confidential internal document you requested...\"}]}]\n", + "output_guardrail_logic(mock_messages)\n", + "print(f\" Result: '{mock_messages[0]['content'][0]['text'][:60]}...'\")\n", + "assert \"cannot provide\" in mock_messages[0][\"content\"][0][\"text\"]\n", + "\n", + "# Test REDACT behavior\n", + "print(\"\\nTest: REDACT — PII redaction in output\")\n", + "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"The user's email is john.doe@example.com and phone is 555-123-4567.\"}]}]\n", + "pii_output_guardrail_logic(mock_messages)\n", + "redacted = mock_messages[0][\"content\"][0][\"text\"]\n", + "print(f\" Result: '{redacted}'\")\n", + "assert \"[REDACTED]\" in redacted\n", + "assert \"john.doe@example.com\" not in redacted\n", + "\n", + "# Test clean content passes\n", + "print(\"\\nTest: Clean content passes through\")\n", + "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"The capital of France is Paris.\"}]}]\n", + "output_guardrail_logic(mock_messages)\n", + "print(f\" Result: Message unchanged (passed)\")\n", + "assert mock_messages[0][\"content\"][0][\"text\"] == \"The capital of France is Paris.\"\n", + "\n", + "# Test combined: BLOCK takes priority\n", + "print(\"\\nTest: Combined — BLOCK takes priority over REDACT\")\n", + "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Run this command: sudo rm -rf /tmp/cache to fix the issue.\"}]}]\n", + "combined_output_guardrail_logic(mock_messages)\n", + "print(f\" Result: '{mock_messages[0]['content'][0]['text'][:60]}...'\")\n", + "assert \"cannot provide\" in mock_messages[0][\"content\"][0][\"text\"]\n", + "\n", + "# Test combined: REDACT when no BLOCK violation\n", + "print(\"\\nTest: Combined — REDACT when no BLOCK violation\")\n", + "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Please contact support at help@company.com for assistance.\"}]}]\n", + "combined_output_guardrail_logic(mock_messages)\n", + "redacted = mock_messages[0][\"content\"][0][\"text\"]\n", + "print(f\" Result: '{redacted}'\")\n", + "assert \"[REDACTED]\" in redacted\n", + "assert \"help@company.com\" not in redacted" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HookProvider: Wrapping Output Guardrails for Agent Registration\n", + "\n", + "In strands-agents 1.40.0, hooks are registered via `HookProvider` classes. The `AfterInvocationEvent` gives access to `event.agent.messages` which contains the full conversation including the assistant's response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class OutputGuardrailHook(HookProvider):\n", + " \"\"\"HookProvider that applies the combined output guardrail after each invocation.\"\"\"\n", + "\n", + " def register_hooks(self, registry: HookRegistry) -> None:\n", + " registry.add_callback(AfterInvocationEvent, self._validate_output)\n", + "\n", + " def _validate_output(self, event: AfterInvocationEvent) -> None:\n", + " messages = event.agent.messages\n", + " combined_output_guardrail_logic(messages)\n", + "\n", + "\n", + "print(\"OutputGuardrailHook defined successfully.\")\n", + "print(\"Register with: Agent(hooks=[OutputGuardrailHook()])\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Attaching to a Live Agent\n", + "\n", + "Register output guardrails using the `hooks` parameter with `HookProvider` instances:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from strands import Agent\n", + " from strands.models.bedrock import BedrockModel\n", + "\n", + " model = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-5-20250929-v1:0\")\n", + "\n", + " agent = Agent(\n", + " model=model,\n", + " system_prompt=\"You are a helpful assistant.\",\n", + " hooks=[OutputGuardrailHook()],\n", + " )\n", + "\n", + " print(\"Agent created with output guardrail attached.\")\n", + " print(\"Testing with a safe request...\")\n", + " response = agent(\"What is the capital of France?\")\n", + " print(f\" Response: {response}\")\n", + "\n", + " print(\"\\nTesting with a request that might produce PII...\")\n", + " response = agent(\"Generate a fake contact card with name, email, and phone number.\")\n", + " print(f\" Response: {response}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Skipping live agent demo: {e}\")\n", + " print(\"(This is expected if no model provider is configured)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook you learned:\n", + "1. **BLOCK behavior** — replace the entire response when prohibited content is detected\n", + "2. **REDACT behavior** — replace only matched patterns (PII) while preserving the rest\n", + "3. How to compose multiple output filters with mixed severity levels\n", + "4. How to test output guardrails with mock messages\n", + "5. How to wrap guardrail logic in a `HookProvider` for agent registration\n", + "6. How to attach output guardrails to a live agent\n", + "\n", + "**Next Steps:** See `03_content_filters.ipynb` to learn how to build custom content filter classes." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb b/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb new file mode 100644 index 00000000..c351195a --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb @@ -0,0 +1,426 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Custom Content Filters\n", + "\n", + "This notebook demonstrates how to build **reusable content filters** that can be composed into guardrail pipelines.\n", + "\n", + "Key concepts:\n", + "- **Severity levels** control what happens when a filter matches: BLOCK, WARN, or REDACT\n", + "- **Filters are composable** \u2014 run multiple filters in sequence and stop at the first violation\n", + "- The **base class pattern** makes it easy to add new filter types\n", + "\n", + "This module is imported by the other notebooks as a shared dependency.\n", + "\n", + "## Filter Pipeline Architecture\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "pip3 install cfn-lint 2>&1 | tail -5 install strands-agents strands-agents-tools hypothesis pytest -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from enum import Enum\n", + "from typing import Optional\n", + "import re" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data Models: Severity and FilterResult\n", + "\n", + "Every filter evaluation returns a `FilterResult` that tells the guardrail what action to take:\n", + "- `Severity.BLOCK` \u2014 reject the entire request/response\n", + "- `Severity.WARN` \u2014 log a warning but allow through\n", + "- `Severity.REDACT` \u2014 remove/replace the matched content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Severity(Enum):\n", + " \"\"\"Action to take when a filter matches.\"\"\"\n", + " BLOCK = \"block\"\n", + " WARN = \"warn\"\n", + " REDACT = \"redact\"\n", + "\n", + "\n", + "@dataclass\n", + "class FilterResult:\n", + " \"\"\"Result of a content filter evaluation.\"\"\"\n", + " passed: bool\n", + " filter_name: str\n", + " severity: Severity\n", + " message: Optional[str] = None\n", + " redacted_text: Optional[str] = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Base Class: ContentFilter\n", + "\n", + "All content filters inherit from this base class. Subclasses must override the `evaluate()` method to implement custom filtering logic." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ContentFilter:\n", + " \"\"\"Base class for content filters.\n", + "\n", + " Subclasses must override the `evaluate()` method to implement\n", + " custom filtering logic.\n", + " \"\"\"\n", + "\n", + " def __init__(self, name: str, severity: Severity = Severity.BLOCK):\n", + " self.name = name\n", + " self.severity = severity\n", + "\n", + " def evaluate(self, text: str) -> FilterResult:\n", + " \"\"\"Evaluate text against this filter. Override in subclasses.\"\"\"\n", + " raise NotImplementedError(\"Subclasses must implement evaluate()\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## RegexContentFilter\n", + "\n", + "Uses regex pattern matching to detect structured sensitive information like emails, phone numbers, and SSNs. When severity is `REDACT`, matched patterns are replaced with `[REDACTED]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class RegexContentFilter(ContentFilter):\n", + " \"\"\"Content filter using regex pattern matching.\"\"\"\n", + "\n", + " def __init__(self, name: str, patterns: list[str], severity: Severity = Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.patterns = [re.compile(p) for p in patterns]\n", + "\n", + " def evaluate(self, text: str) -> FilterResult:\n", + " for pattern in self.patterns:\n", + " if pattern.search(text):\n", + " if self.severity == Severity.REDACT:\n", + " redacted = text\n", + " for p in self.patterns:\n", + " redacted = p.sub(\"[REDACTED]\", redacted)\n", + " return FilterResult(\n", + " passed=False,\n", + " filter_name=self.name,\n", + " severity=self.severity,\n", + " message=f\"Pattern matched: {pattern.pattern}\",\n", + " redacted_text=redacted,\n", + " )\n", + " return FilterResult(\n", + " passed=False,\n", + " filter_name=self.name,\n", + " severity=self.severity,\n", + " message=f\"Pattern matched: {pattern.pattern}\",\n", + " )\n", + " return FilterResult(passed=True, filter_name=self.name, severity=self.severity)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## KeywordContentFilter\n", + "\n", + "Performs case-insensitive matching against a list of prohibited keywords or phrases. Useful for blocking off-topic requests or detecting harmful content categories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class KeywordContentFilter(ContentFilter):\n", + " \"\"\"Content filter using keyword-based topic detection.\"\"\"\n", + "\n", + " def __init__(self, name: str, keywords: list[str], severity: Severity = Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.keywords = [kw.lower() for kw in keywords]\n", + "\n", + " def evaluate(self, text: str) -> FilterResult:\n", + " text_lower = text.lower()\n", + " for keyword in self.keywords:\n", + " if keyword in text_lower:\n", + " return FilterResult(\n", + " passed=False,\n", + " filter_name=self.name,\n", + " severity=self.severity,\n", + " message=f\"Prohibited keyword detected: '{keyword}'\",\n", + " )\n", + " return FilterResult(passed=True, filter_name=self.name, severity=self.severity)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## FormatComplianceFilter\n", + "\n", + "Validates output format compliance \u2014 ensures responses don't include code execution instructions or other format violations. This is an example of a domain-specific filter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FormatComplianceFilter(ContentFilter):\n", + " \"\"\"Content filter that validates output format compliance.\"\"\"\n", + "\n", + " EXECUTION_PATTERNS = [\n", + " re.compile(r\"\\b(run|execute|eval)\\s*\\(\", re.IGNORECASE),\n", + " re.compile(r\"```\\s*(bash|shell|sh)\\b\", re.IGNORECASE),\n", + " re.compile(r\"\\$\\s*\\w+\"), # Shell variable references\n", + " re.compile(r\"sudo\\s+\\w+\", re.IGNORECASE),\n", + " ]\n", + "\n", + " def __init__(self, name: str = \"format_compliance\", severity: Severity = Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + "\n", + " def evaluate(self, text: str) -> FilterResult:\n", + " for pattern in self.EXECUTION_PATTERNS:\n", + " if pattern.search(text):\n", + " return FilterResult(\n", + " passed=False,\n", + " filter_name=self.name,\n", + " severity=self.severity,\n", + " message=f\"Output contains code execution instruction: {pattern.pattern}\",\n", + " )\n", + " return FilterResult(passed=True, filter_name=self.name, severity=self.severity)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipeline Helper: `run_filters()`\n", + "\n", + "Evaluates text against a list of filters in order. The first filter that fails has its result returned immediately. If all filters pass, returns `None`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def run_filters(text: str, filters: list[ContentFilter]) -> Optional[FilterResult]:\n", + " \"\"\"Evaluate text against a list of filters, returning the first violation.\n", + "\n", + " Args:\n", + " text: The text to evaluate.\n", + " filters: Ordered list of content filters to apply.\n", + "\n", + " Returns:\n", + " The FilterResult of the first failing filter, or None if all pass.\n", + " \"\"\"\n", + " for content_filter in filters:\n", + " result = content_filter.evaluate(text)\n", + " if not result.passed:\n", + " return result\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: RegexContentFilter (PII Detection with REDACT)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pii_filter = RegexContentFilter(\n", + " name=\"pii_detector\",\n", + " patterns=[\n", + " r\"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\", # Email\n", + " r\"\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b\", # Phone number\n", + " r\"\\b\\d{3}-\\d{2}-\\d{4}\\b\", # SSN\n", + " ],\n", + " severity=Severity.REDACT,\n", + ")\n", + "\n", + "test_text = \"Contact me at john@example.com or call 555-123-4567\"\n", + "result = pii_filter.evaluate(test_text)\n", + "print(f\"Input: {test_text}\")\n", + "print(f\"Passed: {result.passed}\")\n", + "print(f\"Severity: {result.severity.value}\")\n", + "print(f\"Message: {result.message}\")\n", + "print(f\"Redacted: {result.redacted_text}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: KeywordContentFilter (Topic Blocking)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "topic_filter = KeywordContentFilter(\n", + " name=\"topic_blocker\",\n", + " keywords=[\"hack\", \"exploit\", \"bypass security\"],\n", + " severity=Severity.BLOCK,\n", + ")\n", + "\n", + "# Blocked text\n", + "blocked_text = \"How do I hack into a system?\"\n", + "result = topic_filter.evaluate(blocked_text)\n", + "print(f\"Input: {blocked_text}\")\n", + "print(f\"Passed: {result.passed}\")\n", + "print(f\"Message: {result.message}\")\n", + "\n", + "# Safe text\n", + "safe_text = \"How do I set up a firewall?\"\n", + "result = topic_filter.evaluate(safe_text)\n", + "print(f\"\\nInput: {safe_text}\")\n", + "print(f\"Passed: {result.passed}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: FormatComplianceFilter (Output Validation)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "format_filter = FormatComplianceFilter()\n", + "\n", + "# Unsafe output with code execution instruction\n", + "unsafe_output = \"To fix this, run: sudo rm -rf /tmp/cache\"\n", + "result = format_filter.evaluate(unsafe_output)\n", + "print(f\"Input: {unsafe_output}\")\n", + "print(f\"Passed: {result.passed}\")\n", + "print(f\"Message: {result.message}\")\n", + "\n", + "# Safe output\n", + "safe_output = \"The recommended approach is to clear the cache manually.\"\n", + "result = format_filter.evaluate(safe_output)\n", + "print(f\"\\nInput: {safe_output}\")\n", + "print(f\"Passed: {result.passed}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Pipeline Evaluation with `run_filters()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filters = [topic_filter, pii_filter, format_filter]\n", + "\n", + "# Text that violates the keyword filter (first in the list)\n", + "violation_text = \"How to exploit a vulnerability? Email me at attacker@evil.com\"\n", + "pipeline_result = run_filters(violation_text, filters)\n", + "print(f\"Input: {violation_text}\")\n", + "print(f\"First violation from: {pipeline_result.filter_name}\")\n", + "print(f\"Message: {pipeline_result.message}\")\n", + "\n", + "# Clean text that passes all filters\n", + "clean_text = \"What are best practices for application security?\"\n", + "pipeline_result = run_filters(clean_text, filters)\n", + "print(f\"\\nInput: {clean_text}\")\n", + "print(f\"Result: {'All filters passed' if pipeline_result is None else 'Violation found'}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook you learned:\n", + "1. The `Severity` enum and `FilterResult` dataclass that drive guardrail behavior\n", + "2. The `ContentFilter` base class pattern for building custom filters\n", + "3. `RegexContentFilter` \u2014 pattern-based detection with optional redaction\n", + "4. `KeywordContentFilter` \u2014 case-insensitive keyword/phrase blocking\n", + "5. `FormatComplianceFilter` \u2014 domain-specific output validation\n", + "6. `run_filters()` \u2014 composing filters into a pipeline\n", + "\n", + "**Next Steps:** See `04_guardrail_plugin.ipynb` to learn how to package guardrails as a reusable Plugin." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb b/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb new file mode 100644 index 00000000..49df07b5 --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Guardrail Plugin\n", + "\n", + "This notebook demonstrates how to build a **full-featured guardrail plugin** using the Strands Agents SDK's `HookProvider` pattern.\n", + "\n", + "The plugin bundles input validation, output validation, and tool call enforcement into a single reusable component that can be attached to any agent.\n", + "\n", + "## Plugin Architecture\n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "Key concepts:\n", + "- Implement `HookProvider` to create a reusable guardrail component\n", + "- Register callbacks for `BeforeInvocationEvent`, `AfterInvocationEvent`, and `BeforeToolCallEvent`\n", + "- Combine input, output, and tool call guardrails in one provider\n", + "- Configure filters, tool allowlists, and error handling via constructor\n", + "\n", + "**Registration pattern:**\n", + "```python\n", + "agent = Agent(hooks=[GuardrailPlugin(input_filters=[...], output_filters=[...])])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "!pip install strands-agents strands-agents-tools --upgrade -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "from typing import Optional\n", + "\n", + "from strands.hooks import (\n", + " HookProvider,\n", + " HookRegistry,\n", + " BeforeInvocationEvent,\n", + " AfterInvocationEvent,\n", + " BeforeToolCallEvent,\n", + ")\n", + "\n", + "# Import content filters from our shared module\n", + "import content_filters as _filters_module\n", + "ContentFilter = _filters_module.ContentFilter\n", + "KeywordContentFilter = _filters_module.KeywordContentFilter\n", + "RegexContentFilter = _filters_module.RegexContentFilter\n", + "FormatComplianceFilter = _filters_module.FormatComplianceFilter\n", + "Severity = _filters_module.Severity\n", + "FilterResult = _filters_module.FilterResult\n", + "run_filters = _filters_module.run_filters\n", + "\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.DEBUG, format=\"%(asctime)s [%(levelname)s] %(name)s - %(message)s\", datefmt=\"%H:%M:%S\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The GuardrailPlugin Class\n", + "\n", + "This plugin bundles three types of validation:\n", + "1. **Input validation** — inspects user messages before model inference\n", + "2. **Output validation** — inspects model responses before returning to the user\n", + "3. **Tool call validation** — enforces a tool allowlist before tool execution\n", + "\n", + "Configuration options:\n", + "- `input_filters`: List of ContentFilter instances for user input\n", + "- `output_filters`: List of ContentFilter instances for model output\n", + "- `tool_allowlist`: List of allowed tool names (None = all tools allowed)\n", + "- `fail_open`: If True, filter exceptions allow the request through" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class GuardrailPlugin(HookProvider):\n", + " \"\"\"A reusable HookProvider that applies content guardrails to agent input, output, and tool calls.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " input_filters: list[ContentFilter] | None = None,\n", + " output_filters: list[ContentFilter] | None = None,\n", + " tool_allowlist: list[str] | None = None,\n", + " fail_open: bool = True,\n", + " ):\n", + " self.input_filters = input_filters or []\n", + " self.output_filters = output_filters or []\n", + " self.tool_allowlist = tool_allowlist\n", + " self.fail_open = fail_open\n", + "\n", + " def register_hooks(self, registry: HookRegistry) -> None:\n", + " \"\"\"Register all guardrail callbacks with the hook registry.\"\"\"\n", + " registry.add_callback(BeforeInvocationEvent, self._validate_input)\n", + " registry.add_callback(AfterInvocationEvent, self._validate_output)\n", + " registry.add_callback(BeforeToolCallEvent, self._validate_tool_call)\n", + "\n", + " def _validate_input(self, event: BeforeInvocationEvent) -> None:\n", + " \"\"\"Validate user input before model inference.\"\"\"\n", + " messages = event.agent.messages\n", + " text = self._extract_input_text(messages)\n", + " if not text:\n", + " return\n", + "\n", + " for content_filter in self.input_filters:\n", + " try:\n", + " result = content_filter.evaluate(text)\n", + " if not result.passed:\n", + " self._handle_input_violation(messages, result, text)\n", + " return\n", + " except Exception as e:\n", + " if not self._handle_filter_error(e, content_filter, \"input\"):\n", + " self._block_input(messages, content_filter.name)\n", + " return\n", + "\n", + " self._log_decision(\"input\", \"all_filters\", \"passed\", text)\n", + "\n", + " def _validate_output(self, event: AfterInvocationEvent) -> None:\n", + " \"\"\"Validate model output before returning to user.\"\"\"\n", + " messages = event.agent.messages\n", + " text = self._extract_output_text(messages)\n", + " if not text:\n", + " return\n", + "\n", + " for content_filter in self.output_filters:\n", + " try:\n", + " result = content_filter.evaluate(text)\n", + " if not result.passed:\n", + " self._handle_output_violation(messages, result, text)\n", + " return\n", + " except Exception as e:\n", + " if not self._handle_filter_error(e, content_filter, \"output\"):\n", + " self._block_output(messages, content_filter.name)\n", + " return\n", + "\n", + " self._log_decision(\"output\", \"all_filters\", \"passed\", text)\n", + "\n", + " def _validate_tool_call(self, event: BeforeToolCallEvent) -> None:\n", + " \"\"\"Validate tool calls against the configured allowlist.\"\"\"\n", + " if self.tool_allowlist is None:\n", + " return\n", + "\n", + " tool_name = event.tool_use.get(\"name\", \"\")\n", + "\n", + " if tool_name not in self.tool_allowlist:\n", + " reason = f\"Tool '{tool_name}' is not in the allowed tools list.\"\n", + " event.cancel_tool = reason\n", + " self._log_decision(\"tool_call\", tool_name, \"blocked\", tool_name)\n", + " else:\n", + " self._log_decision(\"tool_call\", tool_name, \"passed\", tool_name)\n", + "\n", + " # --- Helper methods ---\n", + "\n", + " def _extract_input_text(self, messages: list[dict]) -> str:\n", + " if not messages:\n", + " return \"\"\n", + " last_message = messages[-1]\n", + " if last_message.get(\"role\") != \"user\":\n", + " return \"\"\n", + " text_parts = []\n", + " for block in last_message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " text_parts.append(block[\"text\"])\n", + " return \" \".join(text_parts)\n", + "\n", + " def _extract_output_text(self, messages: list[dict]) -> str:\n", + " if not messages:\n", + " return \"\"\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " for block in message.get(\"content\", []):\n", + " if \"text\" in block:\n", + " return block[\"text\"]\n", + " return \"\"\n", + "\n", + " def _handle_input_violation(self, messages, result, original_text):\n", + " if result.severity == Severity.BLOCK:\n", + " self._log_decision(\"input\", result.filter_name, \"blocked\", original_text, result.message)\n", + " messages.clear()\n", + " messages.append({\n", + " \"role\": \"user\",\n", + " \"content\": [{\"text\": (\n", + " \"Respond only with: I cannot process that request. \"\n", + " \"The input was blocked by a content safety filter.\"\n", + " )}],\n", + " })\n", + " elif result.severity == Severity.REDACT:\n", + " self._log_decision(\"input\", result.filter_name, \"redacted\", original_text, result.message)\n", + " if messages and result.redacted_text:\n", + " last_message = messages[-1]\n", + " if last_message.get(\"role\") == \"user\":\n", + " last_message[\"content\"] = [{\"text\": result.redacted_text}]\n", + "\n", + " def _handle_output_violation(self, messages, result, original_text):\n", + " if result.severity == Severity.BLOCK:\n", + " self._log_decision(\"output\", result.filter_name, \"blocked\", original_text, result.message)\n", + " self._replace_assistant_response(\n", + " messages,\n", + " \"I'm sorry, but I cannot provide that information. \"\n", + " \"The response was blocked by a content safety filter.\",\n", + " )\n", + " elif result.severity == Severity.REDACT:\n", + " self._log_decision(\"output\", result.filter_name, \"redacted\", original_text, result.message)\n", + " if result.redacted_text:\n", + " self._replace_assistant_response(messages, result.redacted_text)\n", + "\n", + " def _block_input(self, messages, filter_name):\n", + " messages.clear()\n", + " messages.append({\n", + " \"role\": \"user\",\n", + " \"content\": [{\"text\": (\n", + " \"Respond only with: I cannot process that request. \"\n", + " \"An internal error occurred during content validation.\"\n", + " )}],\n", + " })\n", + "\n", + " def _block_output(self, messages, filter_name):\n", + " self._replace_assistant_response(\n", + " messages,\n", + " \"I'm sorry, but I cannot provide a response at this time. \"\n", + " \"An internal error occurred during content validation.\",\n", + " )\n", + "\n", + " def _replace_assistant_response(self, messages, new_text):\n", + " if not messages:\n", + " return\n", + " for message in reversed(messages):\n", + " if message.get(\"role\") == \"assistant\":\n", + " message[\"content\"] = [{\"text\": new_text}]\n", + " return\n", + "\n", + " def _handle_filter_error(self, error, content_filter, direction):\n", + " if self.fail_open:\n", + " logger.error(f\"Filter error in {direction} (fail-open): {error}\")\n", + " return True\n", + " else:\n", + " logger.error(f\"Filter error in {direction} (fail-closed): {error}\")\n", + " return False\n", + "\n", + " def _log_decision(self, direction, filter_name, action, content, message=None):\n", + " snippet = content[:50] if content else \"\"\n", + " log_msg = f\"[GUARDRAIL] direction={direction} filter={filter_name} action={action} snippet='{snippet}'\"\n", + " if message:\n", + " log_msg += f\" message='{message}'\"\n", + " if action == \"blocked\":\n", + " logger.warning(log_msg)\n", + " else:\n", + " logger.debug(log_msg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Testing Plugin Methods Directly\n", + "\n", + "We can test the plugin's validation methods using mock events — no live model needed.\n", + "\n", + "For unit testing, we call the internal `_validate_*` methods directly with mock event objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Mock event classes for testing\n", + "class MockAgent:\n", + " def __init__(self, messages):\n", + " self.messages = messages\n", + "\n", + "class MockBeforeEvent:\n", + " def __init__(self, messages):\n", + " self.agent = MockAgent(messages)\n", + "\n", + "class MockAfterEvent:\n", + " def __init__(self, messages):\n", + " self.agent = MockAgent(messages)\n", + "\n", + "class MockToolEvent:\n", + " def __init__(self, tool_name, tool_input=None):\n", + " self.tool_use = {\"name\": tool_name, \"input\": tool_input or {}}\n", + " self.cancel_tool = None\n", + "\n", + "\n", + "# Configure the plugin\n", + "plugin = GuardrailPlugin(\n", + " input_filters=[\n", + " KeywordContentFilter(\"prohibited_topics\", [\"hack\", \"exploit\"], Severity.BLOCK),\n", + " RegexContentFilter(\"input_pii\", [r\"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\"], Severity.BLOCK),\n", + " ],\n", + " output_filters=[\n", + " FormatComplianceFilter(\"output_format\", Severity.BLOCK),\n", + " RegexContentFilter(\"output_pii\", [r\"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\"], Severity.REDACT),\n", + " ],\n", + " tool_allowlist=[\"calculator\", \"web_search\", \"file_reader\"],\n", + " fail_open=True,\n", + ")\n", + "\n", + "print(f\"Plugin type: {type(plugin).__name__}\")\n", + "print(f\"Input filters: {[f.name for f in plugin.input_filters]}\")\n", + "print(f\"Output filters: {[f.name for f in plugin.output_filters]}\")\n", + "print(f\"Tool allowlist: {plugin.tool_allowlist}\")\n", + "print(f\"Fail-open: {plugin.fail_open}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test input BLOCK\n", + "print(\"Test: Input BLOCK — prohibited keyword\")\n", + "messages = [{\"role\": \"user\", \"content\": [{\"text\": \"How do I hack a server?\"}]}]\n", + "event = MockBeforeEvent(messages)\n", + "plugin._validate_input(event)\n", + "print(f\" Result: '{event.agent.messages[0]['content'][0]['text'][:60]}...'\")\n", + "assert \"cannot process\" in event.agent.messages[0][\"content\"][0][\"text\"]\n", + "\n", + "# Test input PASS\n", + "print(\"\\nTest: Input PASS — clean content\")\n", + "messages = [{\"role\": \"user\", \"content\": [{\"text\": \"What is cloud computing?\"}]}]\n", + "event = MockBeforeEvent(messages)\n", + "plugin._validate_input(event)\n", + "print(f\" Result: Message unchanged (passed)\")\n", + "assert event.agent.messages[0][\"content\"][0][\"text\"] == \"What is cloud computing?\"\n", + "\n", + "# Test output REDACT\n", + "print(\"\\nTest: Output REDACT — PII in response\")\n", + "messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Contact us at support@company.com for help.\"}]}]\n", + "event = MockAfterEvent(messages)\n", + "plugin._validate_output(event)\n", + "result_text = event.agent.messages[0][\"content\"][0][\"text\"]\n", + "print(f\" Result: '{result_text}'\")\n", + "assert \"[REDACTED]\" in result_text\n", + "\n", + "# Test tool PASS\n", + "print(\"\\nTest: Tool PASS — allowed tool\")\n", + "event = MockToolEvent(\"calculator\")\n", + "plugin._validate_tool_call(event)\n", + "print(f\" Result: Allowed (cancel_tool={event.cancel_tool})\")\n", + "assert event.cancel_tool is None\n", + "\n", + "# Test tool BLOCK\n", + "print(\"\\nTest: Tool BLOCK — disallowed tool\")\n", + "event = MockToolEvent(\"shell_execute\")\n", + "plugin._validate_tool_call(event)\n", + "print(f\" Result: Blocked (cancel_tool='{event.cancel_tool}')\")\n", + "assert event.cancel_tool is not None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Fail-Open vs Fail-Closed Behavior" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class BrokenFilter(ContentFilter):\n", + " \"\"\"A filter that always raises an exception.\"\"\"\n", + " def evaluate(self, text):\n", + " raise RuntimeError(\"Simulated filter failure!\")\n", + "\n", + "\n", + "# Fail-open: broken filter doesn't crash\n", + "print(\"Test: Fail-open — broken filter allows request through\")\n", + "fail_open_plugin = GuardrailPlugin(\n", + " input_filters=[BrokenFilter(\"broken\", Severity.BLOCK)],\n", + " fail_open=True,\n", + ")\n", + "messages = [{\"role\": \"user\", \"content\": [{\"text\": \"Hello world\"}]}]\n", + "event = MockBeforeEvent(messages)\n", + "fail_open_plugin._validate_input(event)\n", + "print(f\" Result: Message unchanged (error caught)\")\n", + "assert event.agent.messages[0][\"content\"][0][\"text\"] == \"Hello world\"\n", + "\n", + "# Fail-closed: broken filter blocks request\n", + "print(\"\\nTest: Fail-closed — broken filter blocks request\")\n", + "fail_closed_plugin = GuardrailPlugin(\n", + " input_filters=[BrokenFilter(\"broken\", Severity.BLOCK)],\n", + " fail_open=False,\n", + ")\n", + "messages = [{\"role\": \"user\", \"content\": [{\"text\": \"Hello world\"}]}]\n", + "event = MockBeforeEvent(messages)\n", + "fail_closed_plugin._validate_input(event)\n", + "print(f\" Result: '{event.agent.messages[0]['content'][0]['text'][:60]}...'\")\n", + "assert \"cannot process\" in event.agent.messages[0][\"content\"][0][\"text\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Attaching to a Live Agent\n", + "\n", + "Register the plugin using the `hooks` parameter:\n", + "\n", + "```python\n", + "agent = Agent(\n", + " system_prompt=\"You are a helpful assistant.\",\n", + " hooks=[plugin],\n", + ")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from strands import Agent\n", + " from strands.models.bedrock import BedrockModel\n", + "\n", + " model = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-5-20250929-v1:0\")\n", + "\n", + " agent = Agent(\n", + " model=model,\n", + " system_prompt=\"You are a helpful assistant.\",\n", + " hooks=[plugin],\n", + " )\n", + "\n", + " print(\"Agent created with GuardrailPlugin attached.\")\n", + " print(\"Testing with a safe request...\")\n", + " response = agent(\"What is the capital of France?\")\n", + " print(f\" Response: {response}\")\n", + "\n", + " print(\"\\nTesting with a prohibited request...\")\n", + " response = agent(\"How do I hack into a system?\")\n", + " print(f\" Response: {response}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Skipping live agent demo: {e}\")\n", + " print(\"(This is expected if no model provider is configured)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook you learned:\n", + "1. How to implement `HookProvider` for reusable guardrail components\n", + "2. Using `register_hooks` to register typed callbacks for multiple event types\n", + "3. Combining input, output, and tool call validation in one provider\n", + "4. Configuring fail-open vs fail-closed error handling\n", + "5. Testing plugin methods with mock events\n", + "\n", + "**Next Steps:** See `05_tool_call_validation.ipynb` to learn about advanced tool call guardrail patterns." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/01-learn/18-input-output-guardrails/05_tool_call_validation.ipynb b/python/01-learn/18-input-output-guardrails/05_tool_call_validation.ipynb new file mode 100644 index 00000000..6dc43aae --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/05_tool_call_validation.ipynb @@ -0,0 +1,506 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tool Call Validation\n", + "\n", + "This notebook demonstrates how to implement **tool call guardrails** using the Strands Agents SDK's `BeforeToolCallEvent` hook.\n", + "\n", + "Tool call guardrails inspect tool names and arguments **before execution**, allowing you to:\n", + "- Enforce **allowlists** (only listed tools can run)\n", + "- Enforce **blocklists** (specific tools are blocked)\n", + "- Validate **arguments** for dangerous inputs (sensitive paths, dangerous commands)\n", + "- Log all tool call decisions for audit\n", + "\n", + "**Key concept:** Use `BeforeToolCallEvent` via a `HookProvider` to intercept tool calls. Access `event.tool_use` dict with keys `name`, `toolUseId`, `input`. Set `event.cancel_tool = \"reason\"` to block a tool call." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "!pip install strands-agents strands-agents-tools --upgrade -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "from typing import Optional\n", + "\n", + "from strands.hooks import HookProvider, HookRegistry, BeforeToolCallEvent\n", + "\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.DEBUG, format=\"%(asctime)s [%(levelname)s] %(name)s - %(message)s\", datefmt=\"%H:%M:%S\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pattern 1: Allowlist Enforcement\n", + "\n", + "Only tools whose names appear in the allowlist are permitted to execute. All other tools are blocked with a descriptive reason." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def allowlist_validate(event, allowed_tools: list[str]) -> None:\n", + " \"\"\"Validate a tool call against an allowlist.\"\"\"\n", + " tool_name = event.tool_use.get(\"name\", \"\")\n", + "\n", + " if tool_name not in allowed_tools:\n", + " reason = f\"Tool '{tool_name}' is not permitted. Allowed tools: {allowed_tools}\"\n", + " event.cancel_tool = reason\n", + " logger.warning(f\"[TOOL GUARDRAIL] BLOCKED tool='{tool_name}' reason='not in allowlist'\")\n", + " else:\n", + " logger.debug(f\"[TOOL GUARDRAIL] ALLOWED tool='{tool_name}' reason='in allowlist'\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pattern 2: Blocklist Enforcement\n", + "\n", + "Tools in the blocklist are blocked. All other tools are allowed. This is the inverse of allowlist — useful when you want to restrict a few dangerous tools but allow everything else." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def blocklist_validate(event, blocked_tools: list[str]) -> None:\n", + " \"\"\"Validate a tool call against a blocklist.\"\"\"\n", + " tool_name = event.tool_use.get(\"name\", \"\")\n", + "\n", + " if tool_name in blocked_tools:\n", + " reason = f\"Tool '{tool_name}' is explicitly blocked.\"\n", + " event.cancel_tool = reason\n", + " logger.warning(f\"[TOOL GUARDRAIL] BLOCKED tool='{tool_name}' reason='in blocklist'\")\n", + " else:\n", + " logger.debug(f\"[TOOL GUARDRAIL] ALLOWED tool='{tool_name}' reason='not in blocklist'\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pattern 3: Argument Validation\n", + "\n", + "Inspects tool arguments to detect dangerous patterns:\n", + "- File operations targeting sensitive paths (`/etc/passwd`, `~/.ssh/`, `.env`)\n", + "- Shell commands containing dangerous patterns (`rm -rf`, `sudo`, `curl | sh`)\n", + "\n", + "This provides defense-in-depth beyond just checking tool names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Sensitive paths that should never be accessed\n", + "SENSITIVE_PATHS = [\n", + " \"/etc/passwd\", \"/etc/shadow\", \"/root/\",\n", + " \"~/.ssh/\", \"~/.aws/credentials\", \".env\",\n", + "]\n", + "\n", + "# Dangerous shell patterns\n", + "DANGEROUS_COMMANDS = [\n", + " \"rm -rf\", \"mkfs\", \"dd if=\", \"> /dev/\",\n", + " \"chmod 777\", \"curl | sh\", \"wget | sh\",\n", + "]\n", + "\n", + "\n", + "def argument_validate(\n", + " event,\n", + " sensitive_paths: Optional[list[str]] = None,\n", + " dangerous_commands: Optional[list[str]] = None,\n", + ") -> None:\n", + " \"\"\"Validate tool arguments for dangerous patterns.\"\"\"\n", + " paths = sensitive_paths or SENSITIVE_PATHS\n", + " commands = dangerous_commands or DANGEROUS_COMMANDS\n", + "\n", + " tool_name = event.tool_use.get(\"name\", \"\")\n", + " tool_input = event.tool_use.get(\"input\", {})\n", + "\n", + " for arg_name, arg_value in tool_input.items():\n", + " if not isinstance(arg_value, str):\n", + " continue\n", + "\n", + " for sensitive_path in paths:\n", + " if sensitive_path in arg_value:\n", + " reason = (\n", + " f\"Tool '{tool_name}' argument '{arg_name}' references \"\n", + " f\"sensitive path: '{sensitive_path}'\"\n", + " )\n", + " event.cancel_tool = reason\n", + " logger.warning(f\"[TOOL GUARDRAIL] BLOCKED tool='{tool_name}' reason='sensitive path'\")\n", + " return\n", + "\n", + " for dangerous_cmd in commands:\n", + " if dangerous_cmd in arg_value:\n", + " reason = (\n", + " f\"Tool '{tool_name}' argument '{arg_name}' contains \"\n", + " f\"dangerous command pattern: '{dangerous_cmd}'\"\n", + " )\n", + " event.cancel_tool = reason\n", + " logger.warning(f\"[TOOL GUARDRAIL] BLOCKED tool='{tool_name}' reason='dangerous command'\")\n", + " return\n", + "\n", + " logger.debug(f\"[TOOL GUARDRAIL] ALLOWED tool='{tool_name}' reason='arguments validated'\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pattern 4: Combined Guardrail with Audit Logging\n", + "\n", + "Combines all three patterns into a single comprehensive guardrail:\n", + "1. Allowlist check (if configured)\n", + "2. Blocklist check (if configured)\n", + "3. Argument validation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def combined_tool_validate(\n", + " event,\n", + " allowed_tools: Optional[list[str]] = None,\n", + " blocked_tools: Optional[list[str]] = None,\n", + " sensitive_paths: Optional[list[str]] = None,\n", + " dangerous_commands: Optional[list[str]] = None,\n", + ") -> None:\n", + " \"\"\"Comprehensive tool call validation combining multiple strategies.\"\"\"\n", + " paths = sensitive_paths or SENSITIVE_PATHS\n", + " commands = dangerous_commands or DANGEROUS_COMMANDS\n", + "\n", + " tool_name = event.tool_use.get(\"name\", \"\")\n", + " tool_input = event.tool_use.get(\"input\", {})\n", + "\n", + " # Step 1: Allowlist check\n", + " if allowed_tools is not None and tool_name not in allowed_tools:\n", + " reason = f\"Tool '{tool_name}' is not in the allowed tools list.\"\n", + " event.cancel_tool = reason\n", + " _audit_log(tool_name, \"BLOCKED\", \"not in allowlist\", tool_input)\n", + " return\n", + "\n", + " # Step 2: Blocklist check\n", + " if blocked_tools is not None and tool_name in blocked_tools:\n", + " reason = f\"Tool '{tool_name}' is explicitly blocked.\"\n", + " event.cancel_tool = reason\n", + " _audit_log(tool_name, \"BLOCKED\", \"in blocklist\", tool_input)\n", + " return\n", + "\n", + " # Step 3: Argument validation\n", + " for arg_name, arg_value in tool_input.items():\n", + " if not isinstance(arg_value, str):\n", + " continue\n", + " for sensitive_path in paths:\n", + " if sensitive_path in arg_value:\n", + " reason = f\"Tool '{tool_name}' blocked: sensitive path '{sensitive_path}'\"\n", + " event.cancel_tool = reason\n", + " _audit_log(tool_name, \"BLOCKED\", f\"sensitive path: {sensitive_path}\", tool_input)\n", + " return\n", + " for dangerous_cmd in commands:\n", + " if dangerous_cmd in arg_value:\n", + " reason = f\"Tool '{tool_name}' blocked: dangerous pattern '{dangerous_cmd}'\"\n", + " event.cancel_tool = reason\n", + " _audit_log(tool_name, \"BLOCKED\", f\"dangerous command: {dangerous_cmd}\", tool_input)\n", + " return\n", + "\n", + " _audit_log(tool_name, \"ALLOWED\", \"all checks passed\", tool_input)\n", + "\n", + "\n", + "def _audit_log(tool_name, decision, reason, tool_input):\n", + " input_summary = {k: str(v)[:50] for k, v in tool_input.items()} if tool_input else {}\n", + " log_msg = f\"[TOOL AUDIT] tool='{tool_name}' decision={decision} reason='{reason}' input={input_summary}\"\n", + " if decision == \"BLOCKED\":\n", + " logger.warning(log_msg)\n", + " else:\n", + " logger.info(log_msg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Testing Each Pattern with Mock Events" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class MockToolEvent:\n", + " \"\"\"Mock BeforeToolCallEvent for demonstration.\"\"\"\n", + " def __init__(self, tool_name: str, tool_input: Optional[dict] = None):\n", + " self.tool_use = {\"name\": tool_name, \"toolUseId\": \"test-123\", \"input\": tool_input or {}}\n", + " self.cancel_tool = None\n", + "\n", + "\n", + "# --- Pattern 1: Allowlist ---\n", + "print(\"--- Pattern 1: Allowlist Enforcement ---\")\n", + "print(\"Only 'calculator' and 'file_reader' are allowed.\\n\")\n", + "\n", + "allowed = [\"calculator\", \"file_reader\"]\n", + "\n", + "event = MockToolEvent(\"calculator\", {\"expression\": \"2 + 2\"})\n", + "allowlist_validate(event, allowed)\n", + "print(f\" Tool: 'calculator' -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "\n", + "event = MockToolEvent(\"shell_execute\", {\"command\": \"ls\"})\n", + "allowlist_validate(event, allowed)\n", + "print(f\" Tool: 'shell_execute' -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "print(f\" Reason: {event.cancel_tool}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# --- Pattern 2: Blocklist ---\n", + "print(\"--- Pattern 2: Blocklist Enforcement ---\")\n", + "print(\"'shell_execute' and 'file_delete' are blocked.\\n\")\n", + "\n", + "blocked = [\"shell_execute\", \"file_delete\"]\n", + "\n", + "event = MockToolEvent(\"calculator\", {\"expression\": \"3 * 7\"})\n", + "blocklist_validate(event, blocked)\n", + "print(f\" Tool: 'calculator' -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "\n", + "event = MockToolEvent(\"shell_execute\", {\"command\": \"whoami\"})\n", + "blocklist_validate(event, blocked)\n", + "print(f\" Tool: 'shell_execute' -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "print(f\" Reason: {event.cancel_tool}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# --- Pattern 3: Argument Validation ---\n", + "print(\"--- Pattern 3: Argument Validation ---\")\n", + "print(\"Blocks tools that access sensitive paths or run dangerous commands.\\n\")\n", + "\n", + "# Safe file read\n", + "event = MockToolEvent(\"file_reader\", {\"path\": \"/tmp/report.txt\"})\n", + "argument_validate(event)\n", + "print(f\" file_reader('/tmp/report.txt') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "\n", + "# Sensitive path\n", + "event = MockToolEvent(\"file_reader\", {\"path\": \"/etc/passwd\"})\n", + "argument_validate(event)\n", + "print(f\" file_reader('/etc/passwd') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "print(f\" Reason: {event.cancel_tool}\")\n", + "\n", + "# Dangerous command\n", + "event = MockToolEvent(\"shell_execute\", {\"command\": \"rm -rf /tmp/data\"})\n", + "argument_validate(event)\n", + "print(f\" shell_execute('rm -rf /tmp/data') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "print(f\" Reason: {event.cancel_tool}\")\n", + "\n", + "# Safe command\n", + "event = MockToolEvent(\"shell_execute\", {\"command\": \"echo hello\"})\n", + "argument_validate(event)\n", + "print(f\" shell_execute('echo hello') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# --- Pattern 4: Combined Guardrail ---\n", + "print(\"--- Pattern 4: Combined Guardrail ---\")\n", + "print(\"Allowlist + argument validation together.\\n\")\n", + "\n", + "# Allowed tool with safe arguments\n", + "event = MockToolEvent(\"calculator\", {\"expression\": \"100 / 4\"})\n", + "combined_tool_validate(event, allowed_tools=[\"calculator\", \"file_reader\", \"shell_execute\"])\n", + "print(f\" calculator('100 / 4') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "\n", + "# Tool not in allowlist\n", + "event = MockToolEvent(\"web_search\", {\"query\": \"test\"})\n", + "combined_tool_validate(event, allowed_tools=[\"calculator\", \"file_reader\", \"shell_execute\"])\n", + "print(f\" web_search('test') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "print(f\" Reason: {event.cancel_tool}\")\n", + "\n", + "# Allowed tool but dangerous arguments\n", + "event = MockToolEvent(\"shell_execute\", {\"command\": \"rm -rf /\"})\n", + "combined_tool_validate(event, allowed_tools=[\"calculator\", \"file_reader\", \"shell_execute\"])\n", + "print(f\" shell_execute('rm -rf /') -> {'ALLOWED' if event.cancel_tool is None else 'BLOCKED'}\")\n", + "print(f\" Reason: {event.cancel_tool}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HookProvider: Wrapping Tool Guardrails for Agent Registration\n", + "\n", + "In strands-agents 1.40.0, hooks are registered via `HookProvider` classes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ToolGuardrailHook(HookProvider):\n", + " \"\"\"HookProvider that validates tool calls using the combined guardrail.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " allowed_tools: Optional[list[str]] = None,\n", + " blocked_tools: Optional[list[str]] = None,\n", + " sensitive_paths: Optional[list[str]] = None,\n", + " dangerous_commands: Optional[list[str]] = None,\n", + " ):\n", + " self.allowed_tools = allowed_tools\n", + " self.blocked_tools = blocked_tools\n", + " self.sensitive_paths = sensitive_paths\n", + " self.dangerous_commands = dangerous_commands\n", + "\n", + " def register_hooks(self, registry: HookRegistry) -> None:\n", + " registry.add_callback(BeforeToolCallEvent, self._validate_tool)\n", + "\n", + " def _validate_tool(self, event: BeforeToolCallEvent) -> None:\n", + " combined_tool_validate(\n", + " event,\n", + " allowed_tools=self.allowed_tools,\n", + " blocked_tools=self.blocked_tools,\n", + " sensitive_paths=self.sensitive_paths,\n", + " dangerous_commands=self.dangerous_commands,\n", + " )\n", + "\n", + "\n", + "print(\"ToolGuardrailHook defined successfully.\")\n", + "print(\"Register with: Agent(hooks=[ToolGuardrailHook(allowed_tools=[...])])\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Attaching to a Live Agent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from strands import Agent, tool\n", + " from strands.models.bedrock import BedrockModel\n", + "\n", + " @tool\n", + " def calculator(expression: str) -> str:\n", + " \"\"\"Evaluate a math expression.\"\"\"\n", + " return str(eval(expression))\n", + "\n", + " @tool\n", + " def file_reader(path: str) -> str:\n", + " \"\"\"Read a file from disk.\"\"\"\n", + " return f\"Contents of {path}\"\n", + "\n", + " @tool\n", + " def shell_execute(command: str) -> str:\n", + " \"\"\"Execute a shell command.\"\"\"\n", + " return f\"Executed: {command}\"\n", + "\n", + " model = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-5-20250929-v1:0\")\n", + "\n", + " tool_hook = ToolGuardrailHook(\n", + " allowed_tools=[\"calculator\", \"file_reader\"],\n", + " sensitive_paths=SENSITIVE_PATHS,\n", + " dangerous_commands=DANGEROUS_COMMANDS,\n", + " )\n", + "\n", + " agent = Agent(\n", + " model=model,\n", + " system_prompt=\"You are a helpful assistant with access to tools.\",\n", + " tools=[calculator, file_reader, shell_execute],\n", + " hooks=[tool_hook],\n", + " )\n", + "\n", + " print(\"Agent created with tool call guardrail.\")\n", + " print(\"Testing: What is 25 * 4?\")\n", + " response = agent(\"What is 25 * 4?\")\n", + " print(f\" Response: {response}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Skipping live agent demo: {e}\")\n", + " print(\"(This is expected if no model provider is configured)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook you learned four tool call validation patterns:\n", + "1. **Allowlist** — only listed tools can run\n", + "2. **Blocklist** — specific tools are blocked, all others allowed\n", + "3. **Argument validation** — inspect arguments for dangerous patterns\n", + "4. **Combined guardrail** — all patterns together with audit logging\n", + "\n", + "And how to wrap them in a `HookProvider` for agent registration.\n", + "\n", + "**Next Steps:** See `06_error_handling.ipynb` to learn about fail-open vs fail-closed error handling patterns." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb b/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb new file mode 100644 index 00000000..d4909fdf --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb @@ -0,0 +1,468 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Error Handling Patterns for Guardrails\n", + "\n", + "This notebook demonstrates how to build **resilient guardrails** that handle errors gracefully.\n", + "\n", + "In production systems, content filters can fail due to timeouts, malformed input, or unexpected exceptions. The key design decision is:\n", + "\n", + "- **Fail-open**: Prioritize availability — if a filter crashes, allow the request through\n", + "- **Fail-closed**: Prioritize safety — if a filter crashes, block the request\n", + "\n", + "This notebook also covers:\n", + "- Structured audit logging for every guardrail decision\n", + "- Graceful handling of malformed messages\n", + "- Comparison of both modes with the same inputs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install required packages\n", + "!pip install strands-agents strands-agents-tools --upgrade -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import time\n", + "from dataclasses import dataclass\n", + "from datetime import datetime, timezone\n", + "from typing import Optional\n", + "\n", + "# Import content filters from our shared module\n", + "import content_filters as _filters_module\n", + "ContentFilter = _filters_module.ContentFilter\n", + "FilterResult = _filters_module.FilterResult\n", + "KeywordContentFilter = _filters_module.KeywordContentFilter\n", + "RegexContentFilter = _filters_module.RegexContentFilter\n", + "Severity = _filters_module.Severity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Audit Logging Setup\n", + "\n", + "Structured audit logging captures every guardrail decision for compliance and debugging." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class AuditEntry:\n", + " \"\"\"Structured audit log entry for a guardrail decision.\"\"\"\n", + " timestamp: str\n", + " direction: str # \"input\", \"output\", or \"tool_call\"\n", + " filter_name: str\n", + " action: str # \"blocked\", \"redacted\", \"warned\", \"passed\", \"error_allow\", \"error_block\"\n", + " content_snippet: str\n", + " message: Optional[str] = None\n", + "\n", + " def to_log_string(self) -> str:\n", + " snippet = self.content_snippet[:50].replace(\"\\n\", \" \")\n", + " parts = [\n", + " f\"direction={self.direction}\",\n", + " f\"filter={self.filter_name}\",\n", + " f\"action={self.action}\",\n", + " f'snippet=\"{snippet}\"',\n", + " ]\n", + " if self.message:\n", + " parts.append(f'message=\"{self.message}\"')\n", + " return \" \".join(parts)\n", + "\n", + "\n", + "# Configure audit logging\n", + "audit_logger = logging.getLogger(\"guardrails.audit\")\n", + "audit_logger.setLevel(logging.DEBUG)\n", + "if not audit_logger.handlers:\n", + " handler = logging.StreamHandler()\n", + " handler.setFormatter(logging.Formatter(\"[GUARDRAIL] %(message)s\"))\n", + " audit_logger.addHandler(handler)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example Filters That Fail\n", + "\n", + "These filters simulate real-world failure scenarios: network errors, timeouts, and bugs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class BrokenFilter(ContentFilter):\n", + " \"\"\"A filter that always raises an exception.\n", + " Simulates network errors, resource exhaustion, or bugs.\"\"\"\n", + "\n", + " def __init__(self, name: str = \"broken_filter\", error_message: str = \"Internal filter error\"):\n", + " super().__init__(name, Severity.BLOCK)\n", + " self.error_message = error_message\n", + "\n", + " def evaluate(self, text: str) -> FilterResult:\n", + " raise RuntimeError(self.error_message)\n", + "\n", + "\n", + "class SlowFilter(ContentFilter):\n", + " \"\"\"A filter that simulates a timeout scenario.\"\"\"\n", + "\n", + " def __init__(self, name: str = \"slow_filter\", delay_seconds: float = 2.0):\n", + " super().__init__(name, Severity.BLOCK)\n", + " self.delay_seconds = delay_seconds\n", + "\n", + " def evaluate(self, text: str) -> FilterResult:\n", + " time.sleep(self.delay_seconds)\n", + " raise TimeoutError(f\"Filter '{self.name}' timed out after {self.delay_seconds}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ResilientGuardrail: Fail-Open vs Fail-Closed\n", + "\n", + "This class wraps content filter execution with error handling and audit logging.\n", + "\n", + "- `fail_open=True` (default): Log the error and allow the request through\n", + "- `fail_open=False`: Log the error and block the request" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ResilientGuardrail:\n", + " \"\"\"A guardrail wrapper that handles filter errors gracefully.\"\"\"\n", + "\n", + " def __init__(self, filters: list[ContentFilter], fail_open: bool = True, direction: str = \"input\"):\n", + " self.filters = filters\n", + " self.fail_open = fail_open\n", + " self.direction = direction\n", + " self.audit_log: list[AuditEntry] = []\n", + " self._logger = logging.getLogger(\"guardrails.audit\")\n", + "\n", + " def _create_audit_entry(self, filter_name, action, content, message=None):\n", + " entry = AuditEntry(\n", + " timestamp=datetime.now(timezone.utc).isoformat(),\n", + " direction=self.direction,\n", + " filter_name=filter_name,\n", + " action=action,\n", + " content_snippet=content[:50] if content else \"\",\n", + " message=message,\n", + " )\n", + " self.audit_log.append(entry)\n", + " return entry\n", + "\n", + " def evaluate(self, text: str) -> tuple[bool, Optional[str], Optional[str]]:\n", + " \"\"\"Evaluate text through all filters with error handling.\n", + "\n", + " Returns:\n", + " (allowed, response_text, redacted_text)\n", + " \"\"\"\n", + " # Handle malformed/empty input\n", + " if text is None:\n", + " entry = self._create_audit_entry(\"input_validation\", \"blocked\", \"\", \"Received None\")\n", + " self._logger.warning(entry.to_log_string())\n", + " return False, \"Invalid input: message content is missing.\", None\n", + "\n", + " if not isinstance(text, str):\n", + " entry = self._create_audit_entry(\"input_validation\", \"blocked\", str(text)[:50],\n", + " f\"Expected string, got {type(text).__name__}\")\n", + " self._logger.warning(entry.to_log_string())\n", + " return False, f\"Invalid input: expected text, got {type(text).__name__}.\", None\n", + "\n", + " if not text.strip():\n", + " entry = self._create_audit_entry(\"input_validation\", \"passed\", \"\", \"Empty message\")\n", + " self._logger.debug(entry.to_log_string())\n", + " return True, None, None\n", + "\n", + " # Run each filter with error handling\n", + " for content_filter in self.filters:\n", + " try:\n", + " result = content_filter.evaluate(text)\n", + "\n", + " if not result.passed:\n", + " if result.severity == Severity.BLOCK:\n", + " entry = self._create_audit_entry(content_filter.name, \"blocked\", text, result.message)\n", + " self._logger.warning(entry.to_log_string())\n", + " return False, f\"Request blocked by '{content_filter.name}': {result.message}\", None\n", + " elif result.severity == Severity.REDACT:\n", + " entry = self._create_audit_entry(content_filter.name, \"redacted\", text, result.message)\n", + " self._logger.info(entry.to_log_string())\n", + " text = result.redacted_text or text\n", + " elif result.severity == Severity.WARN:\n", + " entry = self._create_audit_entry(content_filter.name, \"warned\", text, result.message)\n", + " self._logger.info(entry.to_log_string())\n", + " else:\n", + " entry = self._create_audit_entry(content_filter.name, \"passed\", text)\n", + " self._logger.debug(entry.to_log_string())\n", + "\n", + " except Exception as e:\n", + " if self.fail_open:\n", + " entry = self._create_audit_entry(content_filter.name, \"error_allow\", text, f\"Filter error: {e}\")\n", + " self._logger.error(f\"{entry.to_log_string()} | Allowing request (fail-open).\")\n", + " else:\n", + " entry = self._create_audit_entry(content_filter.name, \"error_block\", text, f\"Filter error: {e}\")\n", + " self._logger.error(f\"{entry.to_log_string()} | Blocking request (fail-closed).\")\n", + " return False, \"Request blocked due to an internal guardrail error.\", None\n", + "\n", + " return True, None, None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Fail-Open Mode\n", + "\n", + "In fail-open mode, a broken filter's error is logged but the request proceeds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set up filters\n", + "keyword_filter = KeywordContentFilter(name=\"topic_blocker\", keywords=[\"hack\", \"exploit\"], severity=Severity.BLOCK)\n", + "pii_filter = RegexContentFilter(\n", + " name=\"pii_redactor\",\n", + " patterns=[r\"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\"],\n", + " severity=Severity.REDACT,\n", + ")\n", + "broken_filter = BrokenFilter(name=\"unstable_classifier\", error_message=\"Connection to ML service refused\")\n", + "\n", + "test_inputs = [\n", + " \"What are best practices for application security?\",\n", + " \"How do I hack into a system?\",\n", + " \"Contact me at user@example.com for details.\",\n", + " \"\",\n", + " \"Normal request that will hit the broken filter\",\n", + "]\n", + "\n", + "print(\"=\" * 60)\n", + "print(\"MODE: FAIL-OPEN (availability > safety)\")\n", + "print(\"Filters: keyword_filter -> broken_filter -> pii_filter\")\n", + "print(\"=\" * 60)\n", + "\n", + "fail_open_guardrail = ResilientGuardrail(\n", + " filters=[keyword_filter, broken_filter, pii_filter],\n", + " fail_open=True,\n", + " direction=\"input\",\n", + ")\n", + "\n", + "for text in test_inputs:\n", + " display_text = text if text else \"\"\n", + " print(f\"\\n Input: \\\"{display_text}\\\"\")\n", + " allowed, message, redacted = fail_open_guardrail.evaluate(text)\n", + " print(f\" Allowed: {allowed}\")\n", + " if message:\n", + " print(f\" Message: {message}\")\n", + " if redacted:\n", + " print(f\" Redacted: {redacted}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Fail-Closed Mode\n", + "\n", + "In fail-closed mode, a broken filter's error causes the request to be blocked." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 60)\n", + "print(\"MODE: FAIL-CLOSED (safety > availability)\")\n", + "print(\"Filters: keyword_filter -> broken_filter -> pii_filter\")\n", + "print(\"=\" * 60)\n", + "\n", + "fail_closed_guardrail = ResilientGuardrail(\n", + " filters=[keyword_filter, broken_filter, pii_filter],\n", + " fail_open=False,\n", + " direction=\"input\",\n", + ")\n", + "\n", + "for text in test_inputs:\n", + " display_text = text if text else \"\"\n", + " print(f\"\\n Input: \\\"{display_text}\\\"\")\n", + " allowed, message, redacted = fail_closed_guardrail.evaluate(text)\n", + " print(f\" Allowed: {allowed}\")\n", + " if message:\n", + " print(f\" Message: {message}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demo: Malformed Message Handling\n", + "\n", + "The guardrail gracefully handles unexpected input types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 60)\n", + "print(\"MALFORMED MESSAGE HANDLING\")\n", + "print(\"=\" * 60)\n", + "\n", + "guardrail = ResilientGuardrail(filters=[keyword_filter], fail_open=True, direction=\"input\")\n", + "\n", + "malformed_inputs = [\n", + " (None, \"None value\"),\n", + " (123, \"Integer instead of string\"),\n", + " ([\"a\", \"list\"], \"List instead of string\"),\n", + " (\"\", \"Empty string\"),\n", + " (\" \", \"Whitespace only\"),\n", + "]\n", + "\n", + "for value, description in malformed_inputs:\n", + " print(f\"\\n Input ({description}): {repr(value)}\")\n", + " allowed, message, _ = guardrail.evaluate(value)\n", + " print(f\" Allowed: {allowed}\")\n", + " if message:\n", + " print(f\" Message: {message}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparison Table: Same Inputs, Different Modes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Suppress audit logging for cleaner output\n", + "audit_logger.setLevel(logging.CRITICAL)\n", + "\n", + "comparison_open = ResilientGuardrail(\n", + " filters=[keyword_filter, broken_filter, pii_filter], fail_open=True, direction=\"input\"\n", + ")\n", + "comparison_closed = ResilientGuardrail(\n", + " filters=[keyword_filter, broken_filter, pii_filter], fail_open=False, direction=\"input\"\n", + ")\n", + "\n", + "print(f\"{'Input':<45} {'Fail-Open':<12} {'Fail-Closed':<12}\")\n", + "print(f\"{'-'*45} {'-'*12} {'-'*12}\")\n", + "\n", + "for text in test_inputs:\n", + " display = (text[:42] + \"...\") if len(text) > 42 else text\n", + " if not display:\n", + " display = \"\"\n", + "\n", + " open_allowed, _, _ = comparison_open.evaluate(text)\n", + " closed_allowed, _, _ = comparison_closed.evaluate(text)\n", + "\n", + " open_status = \"ALLOWED\" if open_allowed else \"BLOCKED\"\n", + " closed_status = \"ALLOWED\" if closed_allowed else \"BLOCKED\"\n", + "\n", + " print(f\"{display:<45} {open_status:<12} {closed_status:<12}\")\n", + "\n", + "# Restore logging\n", + "audit_logger.setLevel(logging.DEBUG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Audit Log Review" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"AUDIT LOG SUMMARY (from fail-open guardrail)\")\n", + "print(\"=\" * 60)\n", + "\n", + "for entry in fail_open_guardrail.audit_log:\n", + " print(f\" [{entry.action.upper():12s}] filter={entry.filter_name:20s} \"\n", + " f'snippet=\"{entry.content_snippet[:30]}...\"')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "**Key Takeaways:**\n", + "\n", + "| Mode | Behavior on Error | Best For |\n", + "|------|-------------------|----------|\n", + "| **Fail-open** | Log error, allow request | User-facing apps, availability-critical systems |\n", + "| **Fail-closed** | Log error, block request | High-security environments, compliance-critical systems |\n", + "\n", + "In this notebook you learned:\n", + "1. The fail-open vs fail-closed design decision\n", + "2. How to build a `ResilientGuardrail` that handles filter errors gracefully\n", + "3. Structured audit logging for compliance\n", + "4. Graceful handling of malformed inputs\n", + "\n", + "**Next Steps:** See `07_testing_guardrails.ipynb` to learn how to test guardrails with example-based and property-based tests." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/01-learn/18-input-output-guardrails/README.md b/python/01-learn/18-input-output-guardrails/README.md new file mode 100644 index 00000000..dc9c3607 --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/README.md @@ -0,0 +1,442 @@ +# Input/Output Guardrails + +Build custom input validation, output validation, and content filtering for Strands Agents using hooks — entirely in Python. + +## Overview + +This tutorial teaches you how to implement guardrails that inspect and control what goes into and comes out of your agent. You'll build: + +- **Input guardrails** that block harmful or non-compliant user messages before they reach the model +- **Output guardrails** that redact PII or replace unsafe responses before they reach the user +- **Tool call guardrails** that restrict which tools the agent can invoke +- **A reusable HookProvider** that bundles all guardrail logic into a single component + +### Architecture + +
+ +
+ +### How is this different from `05-guardrails`? + +The `05-guardrails` tutorial uses **Amazon Bedrock Guardrails** — a managed service you configure through AWS and attach via model parameters. It's great when you want AWS to handle content filtering for you. + +This tutorial takes a different approach: you build guardrails **in pure Python** using the Strands SDK's `HookProvider` infrastructure. This gives you: + +- Full control over validation logic (regex, keywords, custom classifiers) +- Model-agnostic implementation (works with any provider) +- Testable in isolation without a live model connection +- Composable filters you can mix and match per use case + +## Prerequisites + +- Python 3.10+ +- `strands-agents` SDK 1.40.0+ installed +- A configured model provider (AWS Bedrock, Anthropic, etc.) — optional for testing + +Install dependencies: + +```bash +pip install strands-agents strands-agents-tools --upgrade +``` + +## Tutorial Structure + +| File | Description | +|------|-------------| +| `01_input_guardrail.ipynb` | Input validation using `BeforeInvocationEvent` via `HookProvider` | +| `02_output_guardrail.ipynb` | Output validation with BLOCK and REDACT behaviors | +| `03_content_filters.ipynb` | Reusable content filter classes (regex, keyword, format) | +| `04_guardrail_plugin.ipynb` | Full `GuardrailPlugin` class implementing `HookProvider` | +| `05_tool_call_validation.ipynb` | Tool call restriction using `BeforeToolCallEvent` | +| `06_error_handling.ipynb` | Fail-open vs fail-closed error handling patterns | +| `content_filters.py` | Shared content filter module imported by all notebooks | + +--- + +## Step 1: Input Guardrails + +**File:** `01_input_guardrail.ipynb` + +Input guardrails intercept user messages *before* they reach the model. The Strands SDK fires a `BeforeInvocationEvent` at the start of each agent invocation, giving you access to `event.agent.messages` — the full conversation history you can inspect and modify. + +### How it works + +1. Extract text from the last user message +2. Run it through your content filters +3. If a violation is found, replace the messages with a rejection prompt + +### Key code + +```python +from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent + +class InputGuardrailHook(HookProvider): + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(BeforeInvocationEvent, self._validate_input) + + def _validate_input(self, event: BeforeInvocationEvent) -> None: + messages = event.agent.messages + if not messages: + return + + last_message = messages[-1] + if last_message.get("role") != "user": + return + + text = _extract_text_from_message(last_message) + result = keyword_filter.evaluate(text) + + if not result.passed: + # Replace messages with a rejection prompt + messages.clear() + messages.append({ + "role": "user", + "content": [{"text": ( + "Respond only with: I cannot process that request. " + "The input was blocked by a content safety filter." + )}], + }) +``` + +### Registering the hook + +```python +from strands import Agent + +agent = Agent( + system_prompt="You are a helpful assistant.", + hooks=[InputGuardrailHook()], +) +``` + +### Composing multiple filters + +The tutorial also demonstrates a `combined_input_guardrail_logic` that chains keyword detection and PII detection in sequence — the first violation stops evaluation: + +```python +filters = [keyword_filter, pii_filter] +violation = run_filters(text, filters) + +if violation is not None: + # Block the request with the violation's message + messages.clear() + messages.append(...) +``` + +--- + +## Step 2: Output Guardrails + +**File:** `02_output_guardrail.ipynb` + +Output guardrails inspect model responses *after* inference completes. The SDK fires an `AfterInvocationEvent` with `event.agent` — access the conversation history via `event.agent.messages` which includes the assistant's reply. + +### Two behaviors: BLOCK vs REDACT + +| Behavior | What happens | Use case | +|----------|-------------|----------| +| **BLOCK** | Replace the entire response with a safe fallback | Prohibited topics, classified info | +| **REDACT** | Replace only matched patterns with `[REDACTED]` | PII in responses (emails, phone numbers) | + +### BLOCK example + +```python +from strands.hooks import HookProvider, HookRegistry, AfterInvocationEvent + +class OutputGuardrailHook(HookProvider): + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(AfterInvocationEvent, self._validate_output) + + def _validate_output(self, event: AfterInvocationEvent) -> None: + messages = event.agent.messages + # Find last assistant message and evaluate it + response_text = _get_last_assistant_text(messages) + result = output_keyword_filter.evaluate(response_text) + + if not result.passed: + _replace_assistant_response(messages, "Blocked by content filter.") +``` + +### REDACT example + +```python +def pii_output_guardrail_logic(messages: list) -> None: + """Redact PII from model responses.""" + response_text = _get_last_assistant_text(messages) + result = pii_redaction_filter.evaluate(response_text) + + if not result.passed: + _redact_assistant_response(messages, result.redacted_text) +``` + +Given input `"The user's email is john@example.com and phone is 555-123-4567."`, the output becomes: +`"The user's email is [REDACTED] and phone is [REDACTED]."` + +### Registering the hook + +```python +agent = Agent( + system_prompt="You are a helpful assistant.", + hooks=[OutputGuardrailHook()], +) +``` + +--- + +## Step 3: Custom Content Filters + +**File:** `03_content_filters.ipynb` and `content_filters.py` + +Content filters are the building blocks of guardrails. This module defines a composable filter architecture with a base class, concrete implementations, and a pipeline runner. + +### Architecture + +``` +ContentFilter (base class) +├── RegexContentFilter — pattern matching (PII, structured data) +├── KeywordContentFilter — keyword/phrase detection (topic blocking) +└── FormatComplianceFilter — structural validation (no code execution instructions) +``` + +
+ +
+ +### Severity levels + +```python +class Severity(Enum): + BLOCK = "block" # Reject the entire request/response + WARN = "warn" # Log a warning but allow through + REDACT = "redact" # Remove/replace the matched content +``` + +### Pipeline runner + +`run_filters()` evaluates text against a list of filters in order, returning the first violation: + +```python +def run_filters(text: str, filters: list[ContentFilter]) -> Optional[FilterResult]: + """Return the first failing filter's result, or None if all pass.""" + for content_filter in filters: + result = content_filter.evaluate(text) + if not result.passed: + return result + return None +``` + +--- + +## Step 4: Guardrail Plugin (HookProvider) + +**File:** `04_guardrail_plugin.ipynb` + +For production use, package your guardrails as a **HookProvider**. This bundles input, output, and tool call validation into a single reusable component. + +### The HookProvider pattern + +
+ +
+ +```python +from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, AfterInvocationEvent, BeforeToolCallEvent + +class GuardrailPlugin(HookProvider): + def __init__( + self, + input_filters: list[ContentFilter] | None = None, + output_filters: list[ContentFilter] | None = None, + tool_allowlist: list[str] | None = None, + fail_open: bool = True, + ): + self.input_filters = input_filters or [] + self.output_filters = output_filters or [] + self.tool_allowlist = tool_allowlist + self.fail_open = fail_open + + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(BeforeInvocationEvent, self._validate_input) + registry.add_callback(AfterInvocationEvent, self._validate_output) + registry.add_callback(BeforeToolCallEvent, self._validate_tool_call) + + def _validate_input(self, event: BeforeInvocationEvent) -> None: + """Validate user input before model inference.""" + ... + + def _validate_output(self, event: AfterInvocationEvent) -> None: + """Validate model output before returning to user.""" + ... + + def _validate_tool_call(self, event: BeforeToolCallEvent) -> None: + """Validate tool calls against the configured allowlist.""" + ... +``` + +### Key features + +- **`register_hooks`** — registers callbacks for all three event types in one place +- **Configurable filters** — pass different filter lists for input vs output +- **Tool allowlist** — restrict which tools the agent can call (set to `None` to allow all) +- **Fail-open/fail-closed** — control error handling behavior via constructor parameter +- **Audit logging** — all decisions are logged with structured formatting + +### Attaching to an agent + +```python +plugin = GuardrailPlugin( + input_filters=[ + KeywordContentFilter("topics", ["hack", "exploit"], Severity.BLOCK), + RegexContentFilter("pii", [r"\b\d{3}-\d{2}-\d{4}\b"], Severity.BLOCK), + ], + output_filters=[ + RegexContentFilter("pii_redactor", [r"\b\d{3}-\d{2}-\d{4}\b"], Severity.REDACT), + ], + tool_allowlist=["calculator", "web_search"], + fail_open=True, +) + +agent = Agent(hooks=[plugin]) +``` + +--- + +## Step 5: Tool Call Validation + +**File:** `05_tool_call_validation.ipynb` + +Tool call guardrails intercept tool invocations *before* execution using `BeforeToolCallEvent`. This lets you enforce which tools the agent can use and validate their arguments. + +### Event structure + +```python +event.tool_use = { + "name": "shell_execute", + "toolUseId": "abc-123", + "input": {"command": "rm -rf /"} +} +event.cancel_tool = None # Set this to a string to block the tool call +``` + +### Pattern 1: Allowlist + +Only listed tools can execute. Everything else is blocked: + +```python +def allowlist_validate(event, allowed_tools: list[str]) -> None: + tool_name = event.tool_use.get("name", "") + if tool_name not in allowed_tools: + event.cancel_tool = f"Tool '{tool_name}' is not permitted." +``` + +### Pattern 2: Blocklist + +Specific tools are blocked. Everything else is allowed. + +### Pattern 3: Argument validation + +Inspect tool arguments for dangerous patterns (sensitive paths, destructive commands). + +### Registering tool call hooks + +```python +class ToolGuardrailHook(HookProvider): + def __init__(self, allowed_tools=None): + self.allowed_tools = allowed_tools + + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(BeforeToolCallEvent, self._validate_tool) + + def _validate_tool(self, event: BeforeToolCallEvent) -> None: + ... + +agent = Agent( + tools=[calculator, file_reader, shell_execute], + hooks=[ToolGuardrailHook(allowed_tools=["calculator", "file_reader"])], +) +``` + +--- + +## Step 6: Error Handling + +**File:** `06_error_handling.ipynb` + +In production, content filters can fail — network timeouts, malformed input, bugs in custom classifiers. The key design decision: should a filter failure **allow** or **block** the request? + +### Fail-open vs fail-closed + +| Mode | On filter exception | Best for | +|------|-------------------|----------| +| `fail_open=True` | Log error, allow request through | Production systems prioritizing availability | +| `fail_open=False` | Log error, block request | High-security systems prioritizing safety | + +### When to use each + +- **Fail-open**: Customer-facing chatbots, general assistants — a crashed filter shouldn't break the user experience +- **Fail-closed**: Financial compliance, healthcare, legal — safety is non-negotiable + +### Audit logging + +Every guardrail decision is logged with structured formatting for compliance: + +```python +audit_logger.warning( + f"direction=input filter={filter_name} action=blocked " + f'snippet="{content[:50]}" message="{reason}"' +) +``` + +--- + +## Summary + +You've learned how to build a complete guardrail system for Strands Agents: + +1. **Input guardrails** intercept messages via `BeforeInvocationEvent` and block/modify them before inference +2. **Output guardrails** intercept responses via `AfterInvocationEvent` and can BLOCK or REDACT content +3. **Content filters** are composable building blocks with severity levels (BLOCK, WARN, REDACT) +4. **The HookProvider pattern** bundles everything into a reusable component +5. **Tool call validation** restricts which tools the agent can invoke via `BeforeToolCallEvent` +6. **Error handling** lets you choose fail-open (availability) or fail-closed (safety) + +### Key takeaways + +- Guardrails are `HookProvider` classes that register callbacks for lifecycle events +- Access messages via `event.agent.messages` in both `BeforeInvocationEvent` and `AfterInvocationEvent` +- The `hooks=` parameter on `Agent` expects a list of `HookProvider` instances +- Always test guardrails in isolation before deploying with a live model +- Choose fail-open vs fail-closed based on your application's risk profile +- Audit logging is essential for compliance and debugging + +### API Quick Reference (strands-agents 1.40.0) + +```python +from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, AfterInvocationEvent, BeforeToolCallEvent + +class MyHook(HookProvider): + def register_hooks(self, registry: HookRegistry) -> None: + registry.add_callback(BeforeInvocationEvent, self._before) + registry.add_callback(AfterInvocationEvent, self._after) + registry.add_callback(BeforeToolCallEvent, self._tool) + + def _before(self, event: BeforeInvocationEvent) -> None: + messages = event.agent.messages # Access/modify messages + + def _after(self, event: AfterInvocationEvent) -> None: + messages = event.agent.messages # Access/modify messages (includes assistant reply) + + def _tool(self, event: BeforeToolCallEvent) -> None: + event.tool_use # {"name": ..., "toolUseId": ..., "input": {...}} + event.cancel_tool = "reason" # Set to block the tool call + +agent = Agent(hooks=[MyHook()]) +``` + +### Next steps + +- Explore the [hooks tutorial](../06-hooks/) for more lifecycle event patterns +- Check out [05-guardrails](../05-guardrails/) for the managed Bedrock Guardrails approach +- Add custom ML-based classifiers (toxicity, sentiment) as `ContentFilter` subclasses +- Integrate with external moderation APIs by wrapping them in the `ContentFilter` interface diff --git a/python/01-learn/18-input-output-guardrails/images/filter_pipeline.png b/python/01-learn/18-input-output-guardrails/images/filter_pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..c58c3076c5cf63aa26135883aef86cb32644d4a9 GIT binary patch literal 54720 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 LwTcw3w8yht26&szYLYj z=YWPPxX+w9+4V9zgUh}=u^xcHwJIKp19_i(KRHBzSoQ$1j9{kG`-}VT@sW)C9~8>n zhzZE2!hnh@Hq=i%MI_@kiXJv@Ed$Tj*;OG2Vu*o#IpK*=6GbLjPiS$k2QBUo-+`k1 z#`ypaz|=Q!U?GPt)r6y*u7P*)xM>m=P%z=s`z+)Pw+dko{&6Gl_6;qDmJ48VZNFBv zH*g#gi9dgfYENWlNkAZ5o$~HM>f59s=iArCJ~xkU0uNU;&pFe>x<6q%^ddlNZ{5lq z2;_ScD|I9=#QXwo?qMq30tEWC?so6-e6XNBVgr1bdIAI#k0^~Iho9^hbOVjSU?TG! zvT=XJkz}C)&!UgYGAaIYt@(Z+?J6n4vNoay$ol-IdHL8fIxf> z&$<1_tt#LD@kM-MH{fDe3^cm8@ZL$C@Bpa!fs6SA5nWf+_m=GYxX#$4oX8MB+pfSv zwlh8xgCu5xJ$9t{108`RHU=fJQoDQqc%OSaVYnYqFD`z_SHK=$`rGh>Bqm7Y6Seo? zfWkEjpg)#^I{aJ$yMIW4m-n*xqbtA%X$($U1rh6ry}xq`yv+rSBB*n%6MOu}o`+5T z6G_-GA+W|kT@9|V`Tu1IQNW=I=%9q;%_;ElfG2w@auJAk=qrL7ysy+@)Br75=9(Bv_J=n5-r z^E^5Ci3d=Ny_qO4AYiF>X6fMly*Fde;*RgfD1OLeV8Lgd*AJF<#e{eMaRGM>5m3Nz zEB@=7+OLkg-KTOe`vhqm?T(Z?@$?akHv8dM5YQma%eOs%xc}c20TKa+`#08QrCu7z zgFU!I11QA1#Q~vZ*+2;V&Td~mT9-^oBaR0w_W{7)qzeHU!oQCYh41gz9gCXe`f@4fI74* z^x+_hA(Vjy(;43%dH^T_Xdw<)CjZVOUQxb0itGdXD*T671T(Q(S<-)K1J_)HHA6v- zNQY)(C*~0$(%&DSXnQ=57*|9LoOl3f88Jaro7o5q++cFoBzlw( zQ2psx&{Z{to6FIw@L}dY>X2C>2KMy?$L~TTmtK+;o*uwiz+QJd`g&vcQf75mkI>(+ vL7c36+q+m;Si8F~>^3OChf~nn3I{78>gliSUr1ZHF#TwEz2mB2Hwr literal 0 HcmV?d00001 diff --git a/python/01-learn/18-input-output-guardrails/images/guardrail_architecture.png b/python/01-learn/18-input-output-guardrails/images/guardrail_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..29bc2b685c4fbe38173e7376eb214f7dfb722bf3 GIT binary patch literal 62219 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.qwfE36lF^6{Rgaw{wG^cydRY6RQGpow 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«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 literal 0 HcmV?d00001 diff --git a/python/01-learn/18-input-output-guardrails/images/plugin_architecture.png b/python/01-learn/18-input-output-guardrails/images/plugin_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..b114d1a2cd0ce8c8c72be7a599e09c9a3ed239cf GIT binary patch literal 111958 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%0AQlpBI^wmB_4)S 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 literal 0 HcmV?d00001 diff --git a/python/01-learn/18-input-output-guardrails/requirements.txt b/python/01-learn/18-input-output-guardrails/requirements.txt new file mode 100644 index 00000000..80a728e4 --- /dev/null +++ b/python/01-learn/18-input-output-guardrails/requirements.txt @@ -0,0 +1,4 @@ +strands-agents +strands-agents-tools +hypothesis +pytest From c794d6cdcca44300499ad873b0ef49b627c9a0ef Mon Sep 17 00:00:00 2001 From: Syed Sultan Beevi Date: Thu, 28 May 2026 21:50:24 +0530 Subject: [PATCH 2/3] feat(01-learn): add 18-input-output-guardrails tutorial --- python/01-learn/18-input-output-guardrails/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/01-learn/18-input-output-guardrails/README.md b/python/01-learn/18-input-output-guardrails/README.md index dc9c3607..e25eac57 100644 --- a/python/01-learn/18-input-output-guardrails/README.md +++ b/python/01-learn/18-input-output-guardrails/README.md @@ -436,7 +436,7 @@ agent = Agent(hooks=[MyHook()]) ### Next steps -- Explore the [hooks tutorial](../06-hooks/) for more lifecycle event patterns +- Explore the [hooks lifecycle tutorial](../16-hooks-lifecycle/) for more lifecycle event patterns - Check out [05-guardrails](../05-guardrails/) for the managed Bedrock Guardrails approach - Add custom ML-based classifiers (toxicity, sentiment) as `ContentFilter` subclasses - Integrate with external moderation APIs by wrapping them in the `ContentFilter` interface From 476e3461b53bbeec9ee3fb5041d8178225d700fc Mon Sep 17 00:00:00 2001 From: Syed Sultan Beevi Date: Tue, 2 Jun 2026 21:40:16 +0530 Subject: [PATCH 3/3] fix: inline content filter classes and add logger definitions --- .../01_input_guardrail.ipynb | 102 +++++++++++++-- .../02_output_guardrail.ipynb | 118 ++++++++++++++--- .../03_content_filters.ipynb | 2 +- .../04_guardrail_plugin.ipynb | 123 ++++++++++++++---- .../06_error_handling.ipynb | 100 ++++++++++++-- 5 files changed, 381 insertions(+), 64 deletions(-) diff --git a/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb b/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb index 581b6367..7aa18303 100644 --- a/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb +++ b/python/01-learn/18-input-output-guardrails/01_input_guardrail.ipynb @@ -43,6 +43,92 @@ "!pip install strands-agents strands-agents-tools --upgrade -q" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Content filter classes \u2014 inline definitions (no external file needed)\n", + "from dataclasses import dataclass\n", + "from enum import Enum\n", + "from typing import Optional\n", + "import re\n", + "\n", + "class Severity(Enum):\n", + " BLOCK = 'block'\n", + " WARN = 'warn'\n", + " REDACT = 'redact'\n", + "\n", + "@dataclass\n", + "class FilterResult:\n", + " passed: bool\n", + " filter_name: str\n", + " severity: Severity\n", + " message: Optional[str] = None\n", + " redacted_text: Optional[str] = None\n", + "\n", + "class ContentFilter:\n", + " def __init__(self, name, severity=Severity.BLOCK):\n", + " self.name = name\n", + " self.severity = severity\n", + " def evaluate(self, text):\n", + " raise NotImplementedError\n", + "\n", + "class RegexContentFilter(ContentFilter):\n", + " def __init__(self, name, patterns, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.patterns = [re.compile(p) for p in patterns]\n", + " def evaluate(self, text):\n", + " for pattern in self.patterns:\n", + " if pattern.search(text):\n", + " if self.severity == Severity.REDACT:\n", + " redacted = text\n", + " for p in self.patterns:\n", + " redacted = p.sub('[REDACTED]', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class KeywordContentFilter(ContentFilter):\n", + " def __init__(self, name, keywords, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.keywords = [kw.lower() for kw in keywords]\n", + " def evaluate(self, text):\n", + " text_lower = text.lower()\n", + " for keyword in self.keywords:\n", + " if keyword in text_lower:\n", + " return FilterResult(False, self.name, self.severity,\n", + " f\"Prohibited keyword: '{keyword}'\")\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class FormatComplianceFilter(ContentFilter):\n", + " EXECUTION_PATTERNS = [\n", + " re.compile(r'\\b(run|execute|eval)\\s*\\(', re.IGNORECASE),\n", + " re.compile(r'```\\s*(bash|shell|sh)\\b', re.IGNORECASE),\n", + " re.compile(r'sudo\\s+\\w+', re.IGNORECASE),\n", + " ]\n", + " def __init__(self, name='format_compliance', severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " def evaluate(self, text):\n", + " for pattern in self.EXECUTION_PATTERNS:\n", + " if pattern.search(text):\n", + " return FilterResult(False, self.name, self.severity,\n", + " 'Code execution instruction detected')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "def run_filters(text, filters):\n", + " for f in filters:\n", + " result = f.evaluate(text)\n", + " if not result.passed:\n", + " return result\n", + " return None\n", + "\n", + "print('Content filter classes loaded.')" + ] + }, { "cell_type": "code", "execution_count": null, @@ -50,17 +136,11 @@ "outputs": [], "source": [ "import logging\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")\n", "from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent\n", "\n", - "# Import content filters from our shared module\n", - "import content_filters as _filters_module\n", - "KeywordContentFilter = _filters_module.KeywordContentFilter\n", - "RegexContentFilter = _filters_module.RegexContentFilter\n", - "Severity = _filters_module.Severity\n", - "run_filters = _filters_module.run_filters\n", - "\n", - "logger = logging.getLogger(__name__)\n", - "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")" + "# Import content filters from our shared module\n" ] }, { @@ -245,7 +325,7 @@ "source": [ "## Combined Input Guardrail: Multiple Filters in Sequence\n", "\n", - "Demonstrates composing multiple content filters into a single guardrail. Filters are evaluated in order — the first violation stops evaluation and triggers a rejection.\n", + "Demonstrates composing multiple content filters into a single guardrail. Filters are evaluated in order \u2014 the first violation stops evaluation and triggers a rejection.\n", "\n", "Filter order:\n", "1. Keyword filter (blocks prohibited topics)\n", @@ -304,7 +384,7 @@ "source": [ "## Testing the Guardrails\n", "\n", - "We can test guardrail logic directly using mock messages — no live model needed. This simulates how the Strands SDK would call our hook functions." + "We can test guardrail logic directly using mock messages \u2014 no live model needed. This simulates how the Strands SDK would call our hook functions." ] }, { diff --git a/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb b/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb index 2d45422c..f42e0361 100644 --- a/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb +++ b/python/01-learn/18-input-output-guardrails/02_output_guardrail.ipynb @@ -32,6 +32,92 @@ "!pip install strands-agents strands-agents-tools --upgrade -q" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Content filter classes \u2014 inline definitions (no external file needed)\n", + "from dataclasses import dataclass\n", + "from enum import Enum\n", + "from typing import Optional\n", + "import re\n", + "\n", + "class Severity(Enum):\n", + " BLOCK = 'block'\n", + " WARN = 'warn'\n", + " REDACT = 'redact'\n", + "\n", + "@dataclass\n", + "class FilterResult:\n", + " passed: bool\n", + " filter_name: str\n", + " severity: Severity\n", + " message: Optional[str] = None\n", + " redacted_text: Optional[str] = None\n", + "\n", + "class ContentFilter:\n", + " def __init__(self, name, severity=Severity.BLOCK):\n", + " self.name = name\n", + " self.severity = severity\n", + " def evaluate(self, text):\n", + " raise NotImplementedError\n", + "\n", + "class RegexContentFilter(ContentFilter):\n", + " def __init__(self, name, patterns, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.patterns = [re.compile(p) for p in patterns]\n", + " def evaluate(self, text):\n", + " for pattern in self.patterns:\n", + " if pattern.search(text):\n", + " if self.severity == Severity.REDACT:\n", + " redacted = text\n", + " for p in self.patterns:\n", + " redacted = p.sub('[REDACTED]', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class KeywordContentFilter(ContentFilter):\n", + " def __init__(self, name, keywords, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.keywords = [kw.lower() for kw in keywords]\n", + " def evaluate(self, text):\n", + " text_lower = text.lower()\n", + " for keyword in self.keywords:\n", + " if keyword in text_lower:\n", + " return FilterResult(False, self.name, self.severity,\n", + " f\"Prohibited keyword: '{keyword}'\")\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class FormatComplianceFilter(ContentFilter):\n", + " EXECUTION_PATTERNS = [\n", + " re.compile(r'\\b(run|execute|eval)\\s*\\(', re.IGNORECASE),\n", + " re.compile(r'```\\s*(bash|shell|sh)\\b', re.IGNORECASE),\n", + " re.compile(r'sudo\\s+\\w+', re.IGNORECASE),\n", + " ]\n", + " def __init__(self, name='format_compliance', severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " def evaluate(self, text):\n", + " for pattern in self.EXECUTION_PATTERNS:\n", + " if pattern.search(text):\n", + " return FilterResult(False, self.name, self.severity,\n", + " 'Code execution instruction detected')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "def run_filters(text, filters):\n", + " for f in filters:\n", + " result = f.evaluate(text)\n", + " if not result.passed:\n", + " return result\n", + " return None\n", + "\n", + "print('Content filter classes loaded.')" + ] + }, { "cell_type": "code", "execution_count": null, @@ -39,19 +125,11 @@ "outputs": [], "source": [ "import logging\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")\n", "from strands.hooks import HookProvider, HookRegistry, AfterInvocationEvent\n", "\n", - "# Import content filters from our shared module\n", - "import content_filters as _filters_module\n", - "KeywordContentFilter = _filters_module.KeywordContentFilter\n", - "RegexContentFilter = _filters_module.RegexContentFilter\n", - "FormatComplianceFilter = _filters_module.FormatComplianceFilter\n", - "Severity = _filters_module.Severity\n", - "FilterResult = _filters_module.FilterResult\n", - "run_filters = _filters_module.run_filters\n", - "\n", - "logger = logging.getLogger(__name__)\n", - "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")" + "# Import content filters from our shared module\n" ] }, { @@ -230,9 +308,9 @@ "## Combined Output Guardrail: Mixed Behaviors\n", "\n", "Compose multiple output filters with different behaviors:\n", - "1. **Keyword filter (BLOCK)** — replaces entire response if triggered\n", - "2. **Format compliance filter (BLOCK)** — blocks code execution instructions\n", - "3. **PII filter (REDACT)** — redacts matched patterns only\n", + "1. **Keyword filter (BLOCK)** \u2014 replaces entire response if triggered\n", + "2. **Format compliance filter (BLOCK)** \u2014 blocks code execution instructions\n", + "3. **PII filter (REDACT)** \u2014 redacts matched patterns only\n", "\n", "BLOCK-severity violations are checked first. If none are found, REDACT filters clean up the response." ] @@ -308,14 +386,14 @@ "outputs": [], "source": [ "# Test BLOCK behavior\n", - "print(\"Test: BLOCK — Prohibited keyword in output\")\n", + "print(\"Test: BLOCK \u2014 Prohibited keyword in output\")\n", "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Here is the confidential internal document you requested...\"}]}]\n", "output_guardrail_logic(mock_messages)\n", "print(f\" Result: '{mock_messages[0]['content'][0]['text'][:60]}...'\")\n", "assert \"cannot provide\" in mock_messages[0][\"content\"][0][\"text\"]\n", "\n", "# Test REDACT behavior\n", - "print(\"\\nTest: REDACT — PII redaction in output\")\n", + "print(\"\\nTest: REDACT \u2014 PII redaction in output\")\n", "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"The user's email is john.doe@example.com and phone is 555-123-4567.\"}]}]\n", "pii_output_guardrail_logic(mock_messages)\n", "redacted = mock_messages[0][\"content\"][0][\"text\"]\n", @@ -331,14 +409,14 @@ "assert mock_messages[0][\"content\"][0][\"text\"] == \"The capital of France is Paris.\"\n", "\n", "# Test combined: BLOCK takes priority\n", - "print(\"\\nTest: Combined — BLOCK takes priority over REDACT\")\n", + "print(\"\\nTest: Combined \u2014 BLOCK takes priority over REDACT\")\n", "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Run this command: sudo rm -rf /tmp/cache to fix the issue.\"}]}]\n", "combined_output_guardrail_logic(mock_messages)\n", "print(f\" Result: '{mock_messages[0]['content'][0]['text'][:60]}...'\")\n", "assert \"cannot provide\" in mock_messages[0][\"content\"][0][\"text\"]\n", "\n", "# Test combined: REDACT when no BLOCK violation\n", - "print(\"\\nTest: Combined — REDACT when no BLOCK violation\")\n", + "print(\"\\nTest: Combined \u2014 REDACT when no BLOCK violation\")\n", "mock_messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Please contact support at help@company.com for assistance.\"}]}]\n", "combined_output_guardrail_logic(mock_messages)\n", "redacted = mock_messages[0][\"content\"][0][\"text\"]\n", @@ -425,8 +503,8 @@ "## Summary\n", "\n", "In this notebook you learned:\n", - "1. **BLOCK behavior** — replace the entire response when prohibited content is detected\n", - "2. **REDACT behavior** — replace only matched patterns (PII) while preserving the rest\n", + "1. **BLOCK behavior** \u2014 replace the entire response when prohibited content is detected\n", + "2. **REDACT behavior** \u2014 replace only matched patterns (PII) while preserving the rest\n", "3. How to compose multiple output filters with mixed severity levels\n", "4. How to test output guardrails with mock messages\n", "5. How to wrap guardrail logic in a `HookProvider` for agent registration\n", diff --git a/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb b/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb index c351195a..2bd75278 100644 --- a/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb +++ b/python/01-learn/18-input-output-guardrails/03_content_filters.ipynb @@ -36,7 +36,7 @@ "outputs": [], "source": [ "# Install required packages\n", - "pip3 install cfn-lint 2>&1 | tail -5 install strands-agents strands-agents-tools hypothesis pytest -q" + "!pip install strands-agents strands-agents-tools --upgrade -q" ] }, { diff --git a/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb b/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb index 49df07b5..dab05acc 100644 --- a/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb +++ b/python/01-learn/18-input-output-guardrails/04_guardrail_plugin.ipynb @@ -45,6 +45,92 @@ "!pip install strands-agents strands-agents-tools --upgrade -q" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Content filter classes \u2014 inline definitions (no external file needed)\n", + "from dataclasses import dataclass\n", + "from enum import Enum\n", + "from typing import Optional\n", + "import re\n", + "\n", + "class Severity(Enum):\n", + " BLOCK = 'block'\n", + " WARN = 'warn'\n", + " REDACT = 'redact'\n", + "\n", + "@dataclass\n", + "class FilterResult:\n", + " passed: bool\n", + " filter_name: str\n", + " severity: Severity\n", + " message: Optional[str] = None\n", + " redacted_text: Optional[str] = None\n", + "\n", + "class ContentFilter:\n", + " def __init__(self, name, severity=Severity.BLOCK):\n", + " self.name = name\n", + " self.severity = severity\n", + " def evaluate(self, text):\n", + " raise NotImplementedError\n", + "\n", + "class RegexContentFilter(ContentFilter):\n", + " def __init__(self, name, patterns, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.patterns = [re.compile(p) for p in patterns]\n", + " def evaluate(self, text):\n", + " for pattern in self.patterns:\n", + " if pattern.search(text):\n", + " if self.severity == Severity.REDACT:\n", + " redacted = text\n", + " for p in self.patterns:\n", + " redacted = p.sub('[REDACTED]', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class KeywordContentFilter(ContentFilter):\n", + " def __init__(self, name, keywords, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.keywords = [kw.lower() for kw in keywords]\n", + " def evaluate(self, text):\n", + " text_lower = text.lower()\n", + " for keyword in self.keywords:\n", + " if keyword in text_lower:\n", + " return FilterResult(False, self.name, self.severity,\n", + " f\"Prohibited keyword: '{keyword}'\")\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class FormatComplianceFilter(ContentFilter):\n", + " EXECUTION_PATTERNS = [\n", + " re.compile(r'\\b(run|execute|eval)\\s*\\(', re.IGNORECASE),\n", + " re.compile(r'```\\s*(bash|shell|sh)\\b', re.IGNORECASE),\n", + " re.compile(r'sudo\\s+\\w+', re.IGNORECASE),\n", + " ]\n", + " def __init__(self, name='format_compliance', severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " def evaluate(self, text):\n", + " for pattern in self.EXECUTION_PATTERNS:\n", + " if pattern.search(text):\n", + " return FilterResult(False, self.name, self.severity,\n", + " 'Code execution instruction detected')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "def run_filters(text, filters):\n", + " for f in filters:\n", + " result = f.evaluate(text)\n", + " if not result.passed:\n", + " return result\n", + " return None\n", + "\n", + "print('Content filter classes loaded.')" + ] + }, { "cell_type": "code", "execution_count": null, @@ -52,6 +138,8 @@ "outputs": [], "source": [ "import logging\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")\n", "from typing import Optional\n", "\n", "from strands.hooks import (\n", @@ -62,18 +150,7 @@ " BeforeToolCallEvent,\n", ")\n", "\n", - "# Import content filters from our shared module\n", - "import content_filters as _filters_module\n", - "ContentFilter = _filters_module.ContentFilter\n", - "KeywordContentFilter = _filters_module.KeywordContentFilter\n", - "RegexContentFilter = _filters_module.RegexContentFilter\n", - "FormatComplianceFilter = _filters_module.FormatComplianceFilter\n", - "Severity = _filters_module.Severity\n", - "FilterResult = _filters_module.FilterResult\n", - "run_filters = _filters_module.run_filters\n", - "\n", - "logger = logging.getLogger(__name__)\n", - "logging.basicConfig(level=logging.DEBUG, format=\"%(asctime)s [%(levelname)s] %(name)s - %(message)s\", datefmt=\"%H:%M:%S\")" + "# Import content filters from our shared module\n" ] }, { @@ -83,9 +160,9 @@ "## The GuardrailPlugin Class\n", "\n", "This plugin bundles three types of validation:\n", - "1. **Input validation** — inspects user messages before model inference\n", - "2. **Output validation** — inspects model responses before returning to the user\n", - "3. **Tool call validation** — enforces a tool allowlist before tool execution\n", + "1. **Input validation** \u2014 inspects user messages before model inference\n", + "2. **Output validation** \u2014 inspects model responses before returning to the user\n", + "3. **Tool call validation** \u2014 enforces a tool allowlist before tool execution\n", "\n", "Configuration options:\n", "- `input_filters`: List of ContentFilter instances for user input\n", @@ -280,7 +357,7 @@ "source": [ "## Demo: Testing Plugin Methods Directly\n", "\n", - "We can test the plugin's validation methods using mock events — no live model needed.\n", + "We can test the plugin's validation methods using mock events \u2014 no live model needed.\n", "\n", "For unit testing, we call the internal `_validate_*` methods directly with mock event objects." ] @@ -338,7 +415,7 @@ "outputs": [], "source": [ "# Test input BLOCK\n", - "print(\"Test: Input BLOCK — prohibited keyword\")\n", + "print(\"Test: Input BLOCK \u2014 prohibited keyword\")\n", "messages = [{\"role\": \"user\", \"content\": [{\"text\": \"How do I hack a server?\"}]}]\n", "event = MockBeforeEvent(messages)\n", "plugin._validate_input(event)\n", @@ -346,7 +423,7 @@ "assert \"cannot process\" in event.agent.messages[0][\"content\"][0][\"text\"]\n", "\n", "# Test input PASS\n", - "print(\"\\nTest: Input PASS — clean content\")\n", + "print(\"\\nTest: Input PASS \u2014 clean content\")\n", "messages = [{\"role\": \"user\", \"content\": [{\"text\": \"What is cloud computing?\"}]}]\n", "event = MockBeforeEvent(messages)\n", "plugin._validate_input(event)\n", @@ -354,7 +431,7 @@ "assert event.agent.messages[0][\"content\"][0][\"text\"] == \"What is cloud computing?\"\n", "\n", "# Test output REDACT\n", - "print(\"\\nTest: Output REDACT — PII in response\")\n", + "print(\"\\nTest: Output REDACT \u2014 PII in response\")\n", "messages = [{\"role\": \"assistant\", \"content\": [{\"text\": \"Contact us at support@company.com for help.\"}]}]\n", "event = MockAfterEvent(messages)\n", "plugin._validate_output(event)\n", @@ -363,14 +440,14 @@ "assert \"[REDACTED]\" in result_text\n", "\n", "# Test tool PASS\n", - "print(\"\\nTest: Tool PASS — allowed tool\")\n", + "print(\"\\nTest: Tool PASS \u2014 allowed tool\")\n", "event = MockToolEvent(\"calculator\")\n", "plugin._validate_tool_call(event)\n", "print(f\" Result: Allowed (cancel_tool={event.cancel_tool})\")\n", "assert event.cancel_tool is None\n", "\n", "# Test tool BLOCK\n", - "print(\"\\nTest: Tool BLOCK — disallowed tool\")\n", + "print(\"\\nTest: Tool BLOCK \u2014 disallowed tool\")\n", "event = MockToolEvent(\"shell_execute\")\n", "plugin._validate_tool_call(event)\n", "print(f\" Result: Blocked (cancel_tool='{event.cancel_tool}')\")\n", @@ -397,7 +474,7 @@ "\n", "\n", "# Fail-open: broken filter doesn't crash\n", - "print(\"Test: Fail-open — broken filter allows request through\")\n", + "print(\"Test: Fail-open \u2014 broken filter allows request through\")\n", "fail_open_plugin = GuardrailPlugin(\n", " input_filters=[BrokenFilter(\"broken\", Severity.BLOCK)],\n", " fail_open=True,\n", @@ -409,7 +486,7 @@ "assert event.agent.messages[0][\"content\"][0][\"text\"] == \"Hello world\"\n", "\n", "# Fail-closed: broken filter blocks request\n", - "print(\"\\nTest: Fail-closed — broken filter blocks request\")\n", + "print(\"\\nTest: Fail-closed \u2014 broken filter blocks request\")\n", "fail_closed_plugin = GuardrailPlugin(\n", " input_filters=[BrokenFilter(\"broken\", Severity.BLOCK)],\n", " fail_open=False,\n", diff --git a/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb b/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb index d4909fdf..e12e7a08 100644 --- a/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb +++ b/python/01-learn/18-input-output-guardrails/06_error_handling.ipynb @@ -10,8 +10,8 @@ "\n", "In production systems, content filters can fail due to timeouts, malformed input, or unexpected exceptions. The key design decision is:\n", "\n", - "- **Fail-open**: Prioritize availability — if a filter crashes, allow the request through\n", - "- **Fail-closed**: Prioritize safety — if a filter crashes, block the request\n", + "- **Fail-open**: Prioritize availability \u2014 if a filter crashes, allow the request through\n", + "- **Fail-closed**: Prioritize safety \u2014 if a filter crashes, block the request\n", "\n", "This notebook also covers:\n", "- Structured audit logging for every guardrail decision\n", @@ -36,6 +36,92 @@ "!pip install strands-agents strands-agents-tools --upgrade -q" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Content filter classes \u2014 inline definitions (no external file needed)\n", + "from dataclasses import dataclass\n", + "from enum import Enum\n", + "from typing import Optional\n", + "import re\n", + "\n", + "class Severity(Enum):\n", + " BLOCK = 'block'\n", + " WARN = 'warn'\n", + " REDACT = 'redact'\n", + "\n", + "@dataclass\n", + "class FilterResult:\n", + " passed: bool\n", + " filter_name: str\n", + " severity: Severity\n", + " message: Optional[str] = None\n", + " redacted_text: Optional[str] = None\n", + "\n", + "class ContentFilter:\n", + " def __init__(self, name, severity=Severity.BLOCK):\n", + " self.name = name\n", + " self.severity = severity\n", + " def evaluate(self, text):\n", + " raise NotImplementedError\n", + "\n", + "class RegexContentFilter(ContentFilter):\n", + " def __init__(self, name, patterns, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.patterns = [re.compile(p) for p in patterns]\n", + " def evaluate(self, text):\n", + " for pattern in self.patterns:\n", + " if pattern.search(text):\n", + " if self.severity == Severity.REDACT:\n", + " redacted = text\n", + " for p in self.patterns:\n", + " redacted = p.sub('[REDACTED]', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}', redacted)\n", + " return FilterResult(False, self.name, self.severity,\n", + " f'Pattern matched: {pattern.pattern}')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class KeywordContentFilter(ContentFilter):\n", + " def __init__(self, name, keywords, severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " self.keywords = [kw.lower() for kw in keywords]\n", + " def evaluate(self, text):\n", + " text_lower = text.lower()\n", + " for keyword in self.keywords:\n", + " if keyword in text_lower:\n", + " return FilterResult(False, self.name, self.severity,\n", + " f\"Prohibited keyword: '{keyword}'\")\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "class FormatComplianceFilter(ContentFilter):\n", + " EXECUTION_PATTERNS = [\n", + " re.compile(r'\\b(run|execute|eval)\\s*\\(', re.IGNORECASE),\n", + " re.compile(r'```\\s*(bash|shell|sh)\\b', re.IGNORECASE),\n", + " re.compile(r'sudo\\s+\\w+', re.IGNORECASE),\n", + " ]\n", + " def __init__(self, name='format_compliance', severity=Severity.BLOCK):\n", + " super().__init__(name, severity)\n", + " def evaluate(self, text):\n", + " for pattern in self.EXECUTION_PATTERNS:\n", + " if pattern.search(text):\n", + " return FilterResult(False, self.name, self.severity,\n", + " 'Code execution instruction detected')\n", + " return FilterResult(True, self.name, self.severity)\n", + "\n", + "def run_filters(text, filters):\n", + " for f in filters:\n", + " result = f.evaluate(text)\n", + " if not result.passed:\n", + " return result\n", + " return None\n", + "\n", + "print('Content filter classes loaded.')" + ] + }, { "cell_type": "code", "execution_count": null, @@ -43,18 +129,14 @@ "outputs": [], "source": [ "import logging\n", + "logger = logging.getLogger(__name__)\n", + "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\", datefmt=\"%H:%M:%S\")\n", "import time\n", "from dataclasses import dataclass\n", "from datetime import datetime, timezone\n", "from typing import Optional\n", "\n", - "# Import content filters from our shared module\n", - "import content_filters as _filters_module\n", - "ContentFilter = _filters_module.ContentFilter\n", - "FilterResult = _filters_module.FilterResult\n", - "KeywordContentFilter = _filters_module.KeywordContentFilter\n", - "RegexContentFilter = _filters_module.RegexContentFilter\n", - "Severity = _filters_module.Severity" + "# Import content filters from our shared module\n" ] }, {