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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/assets/templates/strands-http-python/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
FROM public.ecr.aws/docker/library/python:3.12-slim-trixie

RUN pip install --no-cache-dir uv

ARG UV_DEFAULT_INDEX
ARG UV_INDEX

WORKDIR /app

ENV UV_SYSTEM_PYTHON=1 \
UV_COMPILE_BYTECODE=1 \
UV_NO_PROGRESS=1 \
PYTHONUNBUFFERED=1 \
DOCKER_CONTAINER=1 \
UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \
UV_INDEX=${UV_INDEX} \
PATH="/app/.venv/bin:$PATH"

RUN useradd -m -u 1000 bedrock_agentcore

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project

COPY --chown=bedrock_agentcore:bedrock_agentcore . .
RUN uv sync --frozen --no-dev

USER bedrock_agentcore

# AgentCore Runtime service contract ports
# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html
# 8080: HTTP Mode
# 8000: MCP Mode
# 9000: A2A Mode
EXPOSE 8080 8000 9000

{{#if enableOtel}}
CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"]
{{else}}
CMD ["python", "-m", "{{entrypoint}}"]
{{/if}}
27 changes: 27 additions & 0 deletions src/assets/templates/strands-http-python/dockerignore.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
dist/
build/

# IDE
.vscode/
.idea/

# Testing
.pytest_cache/
.coverage
htmlcov/

# Secrets and environment files
.env
.env.*

# Version control
.git/

# AgentCore build artifacts
.agentcore/artifacts/
*.zip
13 changes: 9 additions & 4 deletions src/core/project/templates/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,6 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
if (input.protocol !== undefined && input.protocol !== "HTTP")
throw new InputValidationError("the strands-python template only supports HTTP");

if (input.scaffoldRuntimeInput.build !== "CodeZip")
throw new InputValidationError("the strands template only supports CodeZip builds");

const filesystemConfigurations = input.filesystemConfigurations ?? [];
const sessionStorageMountPath = filesystemConfigurations.flatMap((configuration) =>
"sessionStorage" in configuration ? [configuration.sessionStorage.mountPath] : [],
Expand Down Expand Up @@ -115,14 +112,22 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
s3Mounts,
needsOs: filesystemConfigurations.length > 0,
hasConfigBundle: false,
enableOtel: true,
// entrypoint is only consumed on container path, where the docker file attempts to launch it as a module (without .py extension)
entrypoint: input.scaffoldRuntimeInput.entrypoint.replace(/\.py$/, ""),
};
const isContainer = input.scaffoldRuntimeInput.build === "Container";
const tree = await FsTreeNode.fromAssetSource(
{ assetSource },
{ assetDir: "templates/strands-http-python" },
{
rootDirName: input.name,
transformContent: (raw) => templateRenderer.render(raw, context),
filter: (name, isDir) => memory !== undefined || !isDir || name !== "memory",
filter: (name, isDir) => {
if (isDir && name === "memory") return memory !== undefined;
if (name === "Dockerfile" || name === ".dockerignore") return isContainer;
return true;
},
},
);
return {
Expand Down
16 changes: 12 additions & 4 deletions src/handlers/project/add/runtime/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ describe("project add runtime", () => {
"container template build override to CodeZip": {
build: "CodeZip",
},
"strands template overrides to Container": {
build: "Container",
dockerfile: "Dockerfile",
},
"all infrastructure flags": {
description: "Configured runtime",
executionRoleArn: "arn:aws:iam::123456789012:role/MyRole",
Expand Down Expand Up @@ -155,6 +159,10 @@ describe("project add runtime", () => {
"container template build override to CodeZip",
["--name", "my_agent", "--template", "hello-world-python-container", "--build", "CodeZip"],
],
[
"strands template overrides to Container",
["--name", "my_agent", "--template", "strands-python", "--build", "Container"],
],
["custom — all scaffolding flags", ["--name", "my_agent", ...allScaffoldingFlags]],
[
"custom — framework strands",
Expand Down Expand Up @@ -319,6 +327,10 @@ describe("project add runtime", () => {
? flags[buildFlagIndex + 1] === "Container"
: flags.includes("hello-world-python-container");
expect(runtime.runtimeVersion).toBe(isContainer ? undefined : "PYTHON_3_14");
expect(await Bun.file(join(projectRoot, "app", name, "Dockerfile")).exists()).toBe(isContainer);
expect(await Bun.file(join(projectRoot, "app", name, ".dockerignore")).exists()).toBe(
isContainer,
);
});

test.each([
Expand Down Expand Up @@ -386,10 +398,6 @@ describe("project add runtime", () => {
"strands-python only supports HTTP",
["--name", "my_agent", "--template", "strands-python", "--protocol", "MCP"],
],
[
"strands-python only supports CodeZip builds",
["--name", "my_agent", "--template", "strands-python", "--build", "Container"],
],
[
"invalid JSON in --network-config",
["--name", "my_agent", ...template, "--network-config", "{bad}"],
Expand Down
45 changes: 45 additions & 0 deletions src/handlers/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,51 @@ describe("project create", () => {
expect(await Bun.file(join(projectRoot, "app", "custom_agent", "main.py")).exists()).toBe(true);
});

test("scaffolds a Container agent from the strands template", async () => {
const directory = await inTempDirectory();
await run([
"create",
"--name",
"MyProject",
"--template",
"strands-python",
"--build",
"Container",
"--skip-install",
"--skip-git",
]);

const projectRoot = join(directory, "MyProject");
const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json();
expect(spec.runtimes[0]).toMatchObject({
name: "strands_agent",
build: "Container",
codeLocation: "app/strands_agent",
dockerfile: "Dockerfile",
});
expect(spec.runtimes[0].runtimeVersion).toBeUndefined();
const runtimeRoot = join(projectRoot, "app", "strands_agent");
expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(true);
expect(await Bun.file(join(runtimeRoot, ".dockerignore")).exists()).toBe(true);
});

test("omits the Dockerfile from a CodeZip strands template", async () => {
const directory = await inTempDirectory();
await run([
"create",
"--name",
"MyProject",
"--template",
"strands-python",
"--skip-install",
"--skip-git",
]);

const runtimeRoot = join(directory, "MyProject", "app", "strands_agent");
expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(false);
expect(await Bun.file(join(runtimeRoot, ".dockerignore")).exists()).toBe(false);
});

test.each([
["default", [], ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]],
["none", ["--memory", "none"], []],
Expand Down
Loading