Skip to content
Open
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
Empty file.
47 changes: 47 additions & 0 deletions src/assets/templates/strands-http-python/memory/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
import uuid
from typing import Optional

from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryStrategies.length}}, RetrievalConfig{{/if}}
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager

MEMORY_ID = os.getenv("{{memoryEnvVarName}}")
REGION = os.getenv("AWS_REGION")


def get_memory_session_manager(
session_id: Optional[str], actor_id: str
) -> Optional[AgentCoreMemorySessionManager]:
if not MEMORY_ID:
return None

session_id = session_id or uuid.uuid4().hex

{{#if memoryStrategies.length}}
retrieval_config = {
{{#if (includes memoryStrategies "SEMANTIC")}}
f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5),
{{/if}}
{{#if (includes memoryStrategies "USER_PREFERENCE")}}
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
{{/if}}
{{#if (includes memoryStrategies "EPISODIC")}}
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
{{/if}}
{{#if (includes memoryStrategies "SUMMARIZATION")}}
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
{{/if}}
}
{{/if}}

return AgentCoreMemorySessionManager(
AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
session_id=session_id,
actor_id=actor_id,
{{#if memoryStrategies.length}}
retrieval_config=retrieval_config,
{{/if}}
),
REGION,
)
37 changes: 37 additions & 0 deletions src/core/project/__snapshots__/manager.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,49 @@ exports[`FsProjectManager.create snapshots the Strands project manifest and runt
"app/strands_agent/main.py",
"app/strands_agent/mcp_client/__init__.py",
"app/strands_agent/mcp_client/client.py",
"app/strands_agent/memory/__init__.py",
"app/strands_agent/memory/session.py",
"app/strands_agent/model/__init__.py",
"app/strands_agent/model/load.py",
"app/strands_agent/model/mantle_compat.py",
"app/strands_agent/pyproject.toml",
"app/strands_agent/skills/fetcher.py",
],
"memories": [
{
"eventExpiryDuration": 30,
"name": "strands_agentMemory",
"strategies": [
{
"namespaceTemplates": [
"/users/{actorId}/facts",
],
"type": "SEMANTIC",
},
{
"namespaceTemplates": [
"/users/{actorId}/preferences",
],
"type": "USER_PREFERENCE",
},
{
"namespaceTemplates": [
"/summaries/{actorId}/{sessionId}",
],
"type": "SUMMARIZATION",
},
{
"namespaceTemplates": [
"/episodes/{actorId}/{sessionId}",
],
"reflectionNamespaceTemplates": [
"/episodes/{actorId}",
],
"type": "EPISODIC",
},
],
},
],
"runtimes": [
{
"build": "CodeZip",
Expand Down
19 changes: 10 additions & 9 deletions src/core/project/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ import { DeserializationError, ProjectStateError } from "../../errors/errors";
import type { AwsDeploymentTarget } from "../../projectSchemas/aws-targets";
import { ProjectSpecSchema } from "../../projectSchemas/project";
import { FsProjectManager } from "./manager";
import {
RUNTIME_TEMPLATE_SHORTCUTS,
type CreateProjectInput,
type DeployResult,
type Project,
type ProjectEvent,
import { resolveRuntimeTemplateShortcut } from "../../handlers/project/shortcuts";
import type {
CreateProjectInput,
DeployResult,
Project,
ProjectEvent,
} from "../../handlers/project/types";
import { createSilentLogger } from "../../testing";
import type { DeployBackendInput, ProjectBackend } from "./backends/types";

const HELLO_WORLD_PYTHON = RUNTIME_TEMPLATE_SHORTCUTS["hello-world-python"];
const HELLO_WORLD_PYTHON_CONTAINER = RUNTIME_TEMPLATE_SHORTCUTS["hello-world-python-container"];
const STRANDS_PYTHON = RUNTIME_TEMPLATE_SHORTCUTS["strands-python"];
const HELLO_WORLD_PYTHON = resolveRuntimeTemplateShortcut("hello-world-python");
const HELLO_WORLD_PYTHON_CONTAINER = resolveRuntimeTemplateShortcut("hello-world-python-container");
const STRANDS_PYTHON = resolveRuntimeTemplateShortcut("strands-python");

const originalCwd = process.cwd();
const tempDirectories: string[] = [];
Expand Down Expand Up @@ -101,6 +101,7 @@ describe("FsProjectManager.create", () => {
expect({
manifest: await projectManifest(projectRoot),
runtimes: spec.runtimes,
memories: spec.memories,
}).toMatchSnapshot();
});

Expand Down
29 changes: 28 additions & 1 deletion src/core/project/templates/fsTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ describe("FsTreeNode.fromAssetSource", () => {
},
};

const tree = await FsTreeNode.fromAssetSource(source, "template", "root");
const tree = await FsTreeNode.fromAssetSource(
{ assetSource: source },
{ assetDir: "template" },
{ rootDirName: "root" },
);

expect(tree.name).toBe("root");
expect(tree.children.map((node) => node.name)).toEqual(["README.md", "src", ".gitignore"]);
Expand All @@ -83,4 +87,27 @@ describe("FsTreeNode.fromAssetSource", () => {
]);
expect(await tree.children[2]?.bytes?.()).toBe("contents:template/gitignore.template");
});

test("transforms content and filters files and directories", async () => {
const source: AssetSource = {
async list() {
return ["template/keep.txt", "template/skip.txt", "template/optional/nested.txt"];
},
async read(assetPath) {
return `contents:${assetPath}`;
},
};

const tree = await FsTreeNode.fromAssetSource(
{ assetSource: source },
{ assetDir: "template" },
{
transformContent: (content) => content.toUpperCase(),
filter: (name, isDir) => name !== "skip.txt" && !(isDir && name === "optional"),
},
);

expect(tree.children.map(({ name }) => name)).toEqual(["keep.txt"]);
expect(await tree.children[0]?.bytes?.()).toBe("CONTENTS:TEMPLATE/KEEP.TXT");
});
});
52 changes: 35 additions & 17 deletions src/core/project/templates/fsTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,30 @@ export class FsTreeNode {
}

/**
* Expands the flat asset listing under assetDir into a nested tree of nodes.
* Builds a file tree from assets under `input.assetDir`.
*
* @param config - Asset source configuration.
* @param input - Asset directory to load.
* @param options - Optional root name, lazy content transform, and descendant filter. Rejecting a directory omits its subtree.
*/
static async fromAssetSource(
src: AssetSource,
assetDir: string,
rootDirName?: string,
transform?: (content: string) => string,
config: { assetSource: AssetSource },
input: { assetDir: string },
options?: {
rootDirName?: string;
transformContent?: (content: string) => string;
filter?: (name: string, isDir: boolean) => boolean;
},
): Promise<FsTreeNode> {
const paths = await src.list(assetDir);
const { assetSource } = config;
const { assetDir } = input;
const rootDirName = options?.rootDirName;
const transformContent = options?.transformContent;
const filter = options?.filter;
const paths = await assetSource.list(assetDir);
const root = FsTreeNode.createDirectory(rootDirName ?? assetDir, []);

for (const assetPath of paths) {
assetPaths: for (const assetPath of paths) {
const relative = assetPath.slice(assetDir.length + 1);
const segments = relative.split("/");
if (segments.some((s) => s === "" || s === "." || s === "..")) {
Expand All @@ -92,25 +104,31 @@ export class FsTreeNode {
}

let parent = root;
segments.forEach((segment, index) => {
if (index === segments.length - 1) {
for (const [index, segment] of segments.entries()) {
const isDir = index < segments.length - 1;
const name = isDir ? segment : renderName(segment);
// if the segment of a path rejects, reject the rest of the path so we jump to top-loop via assetPaths label.
if (filter && !filter(name, isDir)) continue assetPaths;

if (!isDir) {
parent.children.push(
FsTreeNode.createFile(renderName(segment), async () => {
const raw = await src.read(assetPath);
return transform ? transform(raw) : raw;
FsTreeNode.createFile(name, async () => {
const raw = await assetSource.read(assetPath);
return transformContent ? transformContent(raw) : raw;
}),
);
return;
continue;
}

let child = parent.children.find((n): n is FsTreeNode => n.isDir && n.name === segment);
let child = parent.children.find(
(node): node is FsTreeNode => node.isDir && node.name === name,
);
if (!child) {
child = FsTreeNode.createDirectory(segment, []);
child = FsTreeNode.createDirectory(name, []);
parent.children.push(child);
}

parent = child;
});
}
}

return root;
Expand Down
2 changes: 1 addition & 1 deletion src/core/project/templates/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function createProjectTree(
config.assetSource.read("templates/shared/gitignore.template"),
),
FsTreeNode.createDirectory("agentcore", [
await FsTreeNode.fromAssetSource(config.assetSource, "cdk"),
await FsTreeNode.fromAssetSource({ assetSource: config.assetSource }, { assetDir: "cdk" }),
FsTreeNode.createFile("agentcore.json", async () =>
json({
name: input.projectName,
Expand Down
35 changes: 24 additions & 11 deletions src/core/project/templates/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
if (input.protocol !== undefined && input.protocol !== "HTTP")
throw new InputValidationError(`hello-world-python only supports HTTP protocol`);
const tree = await FsTreeNode.fromAssetSource(
assetSource,
input.scaffoldRuntimeInput.build === "Container"
? "templates/hello-world-python-container"
: "templates/hello-world-python",
input.name,
{ assetSource },
{
assetDir:
input.scaffoldRuntimeInput.build === "Container"
? "templates/hello-world-python-container"
: "templates/hello-world-python",
},
{ rootDirName: input.name },
);
return { tree, spec: { runtimes: [buildRuntimeSpec(input)] } };
},
Expand All @@ -87,10 +90,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
? [{ mountPath: configuration.s3FilesAccessPoint.mountPath }]
: [],
);
const memory = input.scaffoldRuntimeInput.memory;
const context = {
name: toPythonPackageName(input.name),
modelProvider: input.scaffoldRuntimeInput.modelProvider,
hasMemory: input.scaffoldRuntimeInput.memory !== "none",
hasMemory: memory !== undefined,
// the CDK injects this env var corresponding to the actual ID once its resolved on deployment.
memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined,
memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [],
hasIdentity: false,
hasGateway: false,
hasPayment: false,
Expand All @@ -105,14 +112,20 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
hasConfigBundle: false,
};
const tree = await FsTreeNode.fromAssetSource(
assetSource,
"templates/strands-http-python",
input.name,
(raw) => templateRenderer.render(raw, context),
{ assetSource },
{ assetDir: "templates/strands-http-python" },
{
rootDirName: input.name,
transformContent: (raw) => templateRenderer.render(raw, context),
filter: (name, isDir) => memory !== undefined || !isDir || name !== "memory",
},
);
return {
tree,
spec: { runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }] },
spec: {
runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }],
...(memory && { memories: [memory] }),
},
};
},
});
Expand Down
Loading
Loading