diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 68ff7f7a0..cc814a699 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -4,6 +4,9 @@ on: pull_request_target: types: [opened, synchronize, labeled, unlabled, reopened] +env: + AWS_REGION: us-east-1 + jobs: check-access-and-checkout: runs-on: ubuntu-latest @@ -18,27 +21,43 @@ jobs: with: script: | const pr = context.payload.pull_request; - const isOwner = pr.user.type === 'User' && pr.user.login === context.repo.owner; + const labels = pr.labels.map(label => label.name); const hasLabel = labels.includes('approved-for-integ-test') - if (!(hasLabel || isOwner)) { - core.setFailed('Pull Request must either have label approved-for-integ-test or be created by an owner') + if (hasLabel) { + core.info('PR contains label approved-for-integ-test') + return + } + + const isOwner = pr.user.type === 'User' && pr.user.login === context.repo.owner; + if (isOwner) { + core.info('PR auther is an OWNER') + return } + + core.setFailed('Pull Request must either have label approved-for-integ-test or be created by an owner') + - name: Configure Credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.STRANDS_INTEG_TEST_ROLE }} aws-region: us-east-1 - - name: Set LLM Provider Env Vars - uses: aws-actions/aws-secretsmanager-get-secrets@v2 - with: - secret-ids: | - LLAMA_API_KEY, ${{ secrets.STRANDS_LLAMAAPI_API_KEY_SECRET }} - OPENAI_API_KEY, ${{ secrets.STRANDS_OPENAI_API_KEY_SECRET }} - ANTHROPIC_API_KEY, ${{ secrets.STRANDS_ANTHROPIC_API_KEY_SECRET }} + mask-aws-account-id: true - name: Checkout base branch uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} + persist-credentials: false # Don't persist credentials for subsequent actions + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install dependencies + run: | + pip install --no-cache-dir hatch + - name: Run integration tests + id: tests + run: | + hatch test tests-integ diff --git a/README.md b/README.md index ed98d0012..179979138 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- Strands Agents + Strands Agents--------------

diff --git a/pyproject.toml b/pyproject.toml index f593eceb9..bd3097327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "boto3>=1.26.0,<2.0.0", "botocore>=1.29.0,<2.0.0", "docstring_parser>=0.15,<0.16.0", - "mcp>=1.9.3,<2.0.0", + "mcp>=1.8.0,<2.0.0", "pydantic>=2.0.0,<3.0.0", "typing-extensions>=4.13.2,<5.0.0", "watchdog>=6.0.0,<7.0.0", @@ -165,9 +165,6 @@ test = [ test-integ = [ "hatch test tests-integ {args}" ] -test-integ-mcp = [ - "hatch test tests-integ/test_mcp_stdio.py" -] [tool.mypy] diff --git a/tests-integ/conftest.py b/tests-integ/conftest.py new file mode 100644 index 000000000..5b66acadf --- /dev/null +++ b/tests-integ/conftest.py @@ -0,0 +1,6 @@ +import pytest +import time + +@pytest.fixture(autouse=True) +def sleep_to_avoid_throttling(): + time.sleep(5) diff --git a/tests-integ/test_bedrock_guardrails.py b/tests-integ/test_bedrock_guardrails.py index 9ffd1bdf0..bf0be7068 100644 --- a/tests-integ/test_bedrock_guardrails.py +++ b/tests-integ/test_bedrock_guardrails.py @@ -12,7 +12,7 @@ @pytest.fixture(scope="module") def boto_session(): - return boto3.Session(region_name="us-west-2") + return boto3.Session(region_name="us-east-1") @pytest.fixture(scope="module") @@ -142,7 +142,7 @@ def test_guardrail_output_intervention_redact_output(bedrock_guardrail, processi guardrail_stream_processing_mode=processing_mode, guardrail_redact_output=True, guardrail_redact_output_message=REDACT_MESSAGE, - region_name="us-west-2", + region_name="us-east-1", ) agent = Agent( diff --git a/tests-integ/test_mcp_client.py b/tests-integ/test_mcp_client.py index 59ae2a14e..a8995e355 100644 --- a/tests-integ/test_mcp_client.py +++ b/tests-integ/test_mcp_client.py @@ -1,4 +1,5 @@ import base64 +import pytest import threading import time from typing import List, Literal @@ -100,7 +101,7 @@ def test_can_reuse_mcp_client(): tool_use_content_blocks = _messages_to_content_blocks(agent.messages) assert any([block["name"] == "echo" for block in tool_use_content_blocks]) - +@pytest.mark.skip(reason="streamable transport is failing in GitHub actions, debugging if linux compatibility issue") def test_streamable_http_mcp_client(): server_thread = threading.Thread( target=start_calculator_server, kwargs={"transport": "streamable-http", "port": 8001}, daemon=True diff --git a/tests-integ/test_mcp_stdio.py b/tests-integ/test_mcp_stdio.py deleted file mode 100644 index 029cbf73d..000000000 --- a/tests-integ/test_mcp_stdio.py +++ /dev/null @@ -1,110 +0,0 @@ -import logging -import threading -from typing import List, Literal -import platform - -from mcp import StdioServerParameters, stdio_client - -from strands import Agent -from strands.tools.mcp.mcp_client import MCPClient -from strands.tools.mcp.mcp_types import MCPTransport -from strands.types.content import Message -from strands.types.tools import ToolUse - - -logging.getLogger("strands").setLevel(logging.DEBUG) -logger = logging.getLogger(__name__) - - -def start_calculator_server(transport: Literal["sse", "streamable-http"], port=int): - """ - Initialize and start an MCP calculator server for integration testing. - - This function creates a FastMCP server instance that provides a simple - calculator tool for performing addition operations. The server uses - Server-Sent Events (SSE) transport for communication, making it accessible - over HTTP. - """ - from mcp.server import FastMCP - - mcp = FastMCP("Calculator Server", port=port) - - @mcp.tool(description="Calculator tool which performs calculations") - def calculator(x: int, y: int) -> int: - return x + y - - @mcp.tool(description="Generates a custom image") - def generate_custom_image() -> MCPImageContent: - try: - with open("tests-integ/test_image.png", "rb") as image_file: - encoded_image = base64.b64encode(image_file.read()) - return MCPImageContent(type="image", data=encoded_image, mimeType="image/png") - except Exception as e: - print("Error while generating custom image: {}".format(e)) - - mcp.run(transport=transport) - -def get_platform_args(base_args): - """Convert base uvx args to platform-specific format""" - return base_args - -def get_windows_executable_command(command: str) -> str: - """ - Get the correct executable command normalized for Windows. - - On Windows, commands might exist with specific extensions (.exe, .cmd, etc.) - that need to be located for proper execution. - - Args: - command: Base command (e.g., 'uvx', 'npx') - - Returns: - str: Windows-appropriate command path - """ - try: - # First check if command exists in PATH as-is - if command_path := shutil.which(command): - return command_path - - # Check for Windows-specific extensions - for ext in [".cmd", ".bat", ".exe", ".ps1"]: - ext_version = f"{command}{ext}" - if ext_path := shutil.which(ext_version): - return ext_path - - # For regular commands or if we couldn't find special versions - return command - except OSError: - # Handle file system errors during path resolution - # (permissions, broken symlinks, etc.) - return command - -def test_mcp_client(): - """ - Test should yield output similar to the following - {'role': 'user', 'content': [{'text': 'add 1 and 2, then echo the result back to me'}]} - {'role': 'assistant', 'content': [{'text': "I'll help you add 1 and 2 and then echo the result back to you.\n\nFirst, I'll calculate 1 + 2:"}, {'toolUse': {'toolUseId': 'tooluse_17ptaKUxQB20ySZxwgiI_w', 'name': 'calculator', 'input': {'x': 1, 'y': 2}}}]} - {'role': 'user', 'content': [{'toolResult': {'status': 'success', 'toolUseId': 'tooluse_17ptaKUxQB20ySZxwgiI_w', 'content': [{'text': '3'}]}}]} - {'role': 'assistant', 'content': [{'text': "\n\nNow I'll echo the result back to you:"}, {'toolUse': {'toolUseId': 'tooluse_GlOc5SN8TE6ti8jVZJMBOg', 'name': 'echo', 'input': {'to_echo': '3'}}}]} - {'role': 'user', 'content': [{'toolResult': {'status': 'success', 'toolUseId': 'tooluse_GlOc5SN8TE6ti8jVZJMBOg', 'content': [{'text': '3'}]}}]} - {'role': 'assistant', 'content': [{'text': '\n\nThe result of adding 1 and 2 is 3.'}]} - """ # noqa: E501 - - - print("STARTING STDIO") - print(f"WINDOWS {get_windows_executable_command("uvx")}) - logger.info("STARTING STDIO_STDIO") - stdio_mcp_client = MCPClient( - lambda: stdio_client( - StdioServerParameters( - command="uvx", - args=get_platform_args(["awslabs.aws-documentation-mcp-server@latest"]) - ) - ) - ) - with stdio_mcp_client: - agent = Agent(tools=stdio_mcp_client.list_tools_sync()) - logger.debug(f"Tools {agent.tool_names}") - print(f"Tools {agent.tool_names}") - print("DONE") - assert 1 == 2 diff --git a/tests-integ/test_model_litellm.py b/tests-integ/test_model_litellm.py index f1afb61fa..8c7413e00 100644 --- a/tests-integ/test_model_litellm.py +++ b/tests-integ/test_model_litellm.py @@ -7,7 +7,7 @@ @pytest.fixture def model(): - return LiteLLMModel(model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0") + return LiteLLMModel(model_id="bedrock/anthropic.claude-3-sonnet-20240229-v1:0") @pytest.fixture