@aws/agentcore 1.0.0-rc.1 → 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +175 -0
- package/README.md +93 -985
- package/dist/assets/agent-inspector/index.js.asset +1 -1
- package/dist/assets/cdk/README.md +42 -19
- package/dist/assets/cdk/bin/cdk.ts +22 -173
- package/dist/assets/cdk/lib/cdk-stack.ts +18 -80
- package/dist/assets/cdk/package.json +3 -10
- package/dist/assets/cdk/tsconfig.json +1 -1
- package/dist/assets/evaluators/python-lambda/README.md +5 -1
- package/dist/assets/templates/a2a-python-strands/pyproject.toml +1 -1
- package/dist/assets/templates/agent-python-langchain/README.md +5 -4
- package/dist/assets/templates/agent-python-langchain/main.py +7 -11
- package/dist/assets/templates/agent-python-strands/README.md +16 -2
- package/dist/assets/templates/agent-python-strands/main.py +40 -81
- package/dist/assets/templates/agent-python-strands/memory/session.py +1 -5
- package/dist/assets/templates/agent-python-strands/model/load.py +4 -4
- package/dist/assets/templates/agent-python-strands/parse.py +41 -0
- package/dist/assets/templates/agent-typescript-strands/README.md +23 -2
- package/dist/assets/templates/agent-typescript-strands/main.ts +4 -13
- package/dist/assets/templates/agent-typescript-strands/memory/memory.ts +1 -12
- package/dist/assets/templates/agent-typescript-strands/package.json.template +0 -1
- package/dist/assets/templates/agui-python-strands/README.md +1 -1
- package/dist/assets/templates/agui-python-strands/pyproject.toml +1 -1
- package/dist/assets/templates/export-harness-python/model/load.py +3 -3
- package/dist/assets/templates/harness/harness.yaml +225 -0
- package/dist/main.js +728 -220
- package/package.json +13 -6
- package/dist/assets/cdk/.prettierrc +0 -8
- package/dist/assets/cdk/jest.config.js +0 -9
- package/dist/assets/cdk/npmignore.template +0 -6
- package/dist/assets/cdk/test/cdk.test.ts +0 -120
- package/dist/assets/evaluators/autoevals-lambda/README.md +0 -23
- package/dist/assets/evaluators/autoevals-lambda/execution-role-policy.json +0 -15
- package/dist/assets/evaluators/autoevals-lambda/lambda_function.py +0 -37
- package/dist/assets/evaluators/autoevals-lambda/pyproject.toml +0 -23
- package/dist/assets/evaluators/deepeval-lambda/README.md +0 -23
- package/dist/assets/evaluators/deepeval-lambda/execution-role-policy.json +0 -15
- package/dist/assets/evaluators/deepeval-lambda/lambda_function.py +0 -29
- package/dist/assets/evaluators/deepeval-lambda/pyproject.toml +0 -22
- package/dist/assets/templates/agent-typescript-strands/mcp_client/client.ts +0 -11
|
@@ -1,181 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
|
|
8
|
-
function toEnvironment(target: AwsDeploymentTarget): Environment {
|
|
9
|
-
return {
|
|
10
|
-
account: target.account,
|
|
11
|
-
region: target.region,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function sanitize(name: string): string {
|
|
16
|
-
return name.replace(/_/g, '-');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function toStackName(projectName: string, targetName: string): string {
|
|
20
|
-
return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// The vended CDK project compiles against the published @aws/agentcore-cdk schema
|
|
24
|
-
// type, which may lag the CLI's own AgentCoreProjectSpec (e.g. payments, harnesses,
|
|
25
|
-
// gateway fields). This alias documents each read of those not-yet-published fields.
|
|
26
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
-
type SpecWithLatestFields = any;
|
|
28
|
-
|
|
29
|
-
// Extract MCP configuration from the project spec. Gateway fields are stored in
|
|
30
|
-
// agentcore.json but may not yet be on the published spec type, so they are read
|
|
31
|
-
// off the loosened alias.
|
|
32
|
-
function resolveMcpSpec(spec: SpecWithLatestFields) {
|
|
33
|
-
return spec.agentCoreGateways?.length
|
|
34
|
-
? {
|
|
35
|
-
agentCoreGateways: spec.agentCoreGateways,
|
|
36
|
-
mcpRuntimeTools: spec.mcpRuntimeTools,
|
|
37
|
-
unassignedTargets: spec.unassignedTargets,
|
|
38
|
-
}
|
|
39
|
-
: undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Read non-S3 KB connector-config files and return their parsed contents keyed by
|
|
43
|
-
// the data source's connectorConfigFile path. The L3 does not read files; it
|
|
44
|
-
// expects these parsed connectorParameters verbatim.
|
|
45
|
-
function resolveConnectorParametersByFile(
|
|
46
|
-
spec: SpecWithLatestFields,
|
|
47
|
-
projectRoot: string
|
|
48
|
-
): Record<string, Record<string, unknown>> {
|
|
49
|
-
const connectorParametersByFile: Record<string, Record<string, unknown>> = {};
|
|
50
|
-
for (const kb of spec.knowledgeBases ?? []) {
|
|
51
|
-
for (const ds of kb.dataSources ?? []) {
|
|
52
|
-
if (ds.type !== 'S3' && ds.connectorConfigFile) {
|
|
53
|
-
const abs = path.resolve(projectRoot, ds.connectorConfigFile);
|
|
54
|
-
try {
|
|
55
|
-
connectorParametersByFile[ds.connectorConfigFile] = JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
56
|
-
} catch (err) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Could not read connector config '${ds.connectorConfigFile}' for knowledge base '${kb.name}' at ${abs}: ${err instanceof Error ? err.message : err}`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return connectorParametersByFile;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Synthesize a HarnessConfig for each harness entry in the spec. The full validated
|
|
68
|
-
// spec drives the AWS::BedrockAgentCore::Harness CFN resource; the role-scoped
|
|
69
|
-
// fields drive the IAM role + container build.
|
|
70
|
-
function resolveHarnessConfigs(spec: SpecWithLatestFields, projectRoot: string): HarnessConfig[] {
|
|
71
|
-
const harnessConfigs: HarnessConfig[] = [];
|
|
72
|
-
for (const entry of spec.harnesses ?? []) {
|
|
73
|
-
const harnessDir = path.resolve(projectRoot, entry.path);
|
|
74
|
-
const harnessPath = path.resolve(harnessDir, 'harness.json');
|
|
75
|
-
try {
|
|
76
|
-
const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8')));
|
|
77
|
-
harnessConfigs.push({
|
|
78
|
-
name: entry.name,
|
|
79
|
-
executionRoleArn: harnessSpec.executionRoleArn,
|
|
80
|
-
// Only an `existing` memory ref carries a name to wire IAM against; managed memory is
|
|
81
|
-
// owned by the harness (no sibling) and disabled has none — both resolve to undefined.
|
|
82
|
-
memoryName: harnessSpec.memory?.mode === 'existing' ? harnessSpec.memory.name : undefined,
|
|
83
|
-
containerUri: harnessSpec.containerUri,
|
|
84
|
-
hasDockerfile: !!harnessSpec.dockerfile,
|
|
85
|
-
dockerfile: harnessSpec.dockerfile,
|
|
86
|
-
codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
|
|
87
|
-
tools: harnessSpec.tools,
|
|
88
|
-
skills: harnessSpec.skills,
|
|
89
|
-
apiKeyArn: harnessSpec.model?.apiKeyArn,
|
|
90
|
-
efsAccessPoints: harnessSpec.efsAccessPoints,
|
|
91
|
-
s3AccessPoints: harnessSpec.s3AccessPoints,
|
|
92
|
-
apiFormat: harnessSpec.model?.apiFormat,
|
|
93
|
-
// Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource.
|
|
94
|
-
spec: harnessSpec,
|
|
95
|
-
harnessDir,
|
|
96
|
-
});
|
|
97
|
-
} catch (err) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return harnessConfigs;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function main() {
|
|
107
|
-
// Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/.
|
|
108
|
-
const configRoot = path.resolve(process.cwd(), '..');
|
|
109
|
-
const configIO = new ConfigIO({ baseDir: configRoot });
|
|
110
|
-
|
|
111
|
-
const spec = await configIO.readProjectSpec();
|
|
112
|
-
const targets = await configIO.readAWSDeploymentTargets();
|
|
113
|
-
|
|
114
|
-
// `project build` runs before a project has anywhere to deploy, so an empty target
|
|
115
|
-
// list is not an error: it synthesizes a single environment-agnostic stack, which is
|
|
116
|
-
// enough to typecheck the app and produce a template. Only a stack synthesized for a
|
|
117
|
-
// real target is a deploy candidate; the target tag below is what marks one.
|
|
118
|
-
const stackTargets: (AwsDeploymentTarget | undefined)[] = targets.length > 0 ? targets : [undefined];
|
|
119
|
-
|
|
120
|
-
const specAny: SpecWithLatestFields = spec;
|
|
121
|
-
const projectRoot = path.resolve(configRoot, '..');
|
|
122
|
-
|
|
123
|
-
const mcpSpec = resolveMcpSpec(specAny);
|
|
124
|
-
const connectorParametersByFile = resolveConnectorParametersByFile(specAny, projectRoot);
|
|
125
|
-
const harnessConfigs = resolveHarnessConfigs(specAny, projectRoot);
|
|
126
|
-
|
|
127
|
-
// Read deployed state for credential ARNs (populated by pre-deploy identity setup).
|
|
128
|
-
// Under agentcore/.cli/ to match the released CLI's location.
|
|
129
|
-
let deployedState: Record<string, unknown> | undefined;
|
|
130
|
-
try {
|
|
131
|
-
deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8'));
|
|
132
|
-
} catch (err) {
|
|
133
|
-
// A missing file is the normal first-deploy case. A malformed one is not:
|
|
134
|
-
// surface it rather than silently synthesizing without the credential ARNs
|
|
135
|
-
// it holds (which would drop them from the stack).
|
|
136
|
-
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
137
|
-
}
|
|
2
|
+
import { App } from 'aws-cdk-lib';
|
|
3
|
+
import { readAgentCoreProject, resolveTargetStacks, transformAgentCoreJson } from '@aws/agentcore-cdk';
|
|
4
|
+
import { AgentCoreStack } from '../lib/cdk-stack';
|
|
138
5
|
|
|
6
|
+
try {
|
|
7
|
+
// The AgentCore CLI runs this app from agentcore/cdk/; readAgentCoreProject walks up to agentcore/.
|
|
8
|
+
const project = readAgentCoreProject();
|
|
139
9
|
const app = new App();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Record<string, { credentialProviderArn: string; clientSecretArn?: string }> | undefined;
|
|
155
|
-
|
|
156
|
-
new AgentCoreStack(app, stackName, {
|
|
157
|
-
spec,
|
|
158
|
-
mcpSpec,
|
|
159
|
-
credentials,
|
|
160
|
-
connectorParametersByFile,
|
|
161
|
-
harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
|
|
162
|
-
env,
|
|
163
|
-
description: target
|
|
164
|
-
? `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`
|
|
165
|
-
: `AgentCore stack for ${spec.name} (no deployment target configured)`,
|
|
166
|
-
// Only a stack synthesized for a real target carries the target tag, which is
|
|
167
|
-
// how deploy selects the stack to ship.
|
|
168
|
-
tags: {
|
|
169
|
-
'agentcore:project-name': spec.name,
|
|
170
|
-
...(target ? { 'agentcore:target-name': target.name } : {}),
|
|
171
|
-
},
|
|
10
|
+
for (const stack of resolveTargetStacks({
|
|
11
|
+
projectName: project.projectName,
|
|
12
|
+
targets: project.targets,
|
|
13
|
+
deployedState: project.deployedState,
|
|
14
|
+
})) {
|
|
15
|
+
new AgentCoreStack(app, stack.stackName, {
|
|
16
|
+
env: stack.env,
|
|
17
|
+
tags: stack.tags,
|
|
18
|
+
description: stack.description,
|
|
19
|
+
application: transformAgentCoreJson(project.agentCoreJson, {
|
|
20
|
+
projectRoot: project.projectRoot,
|
|
21
|
+
credentials: stack.credentials,
|
|
22
|
+
target: stack.target,
|
|
23
|
+
}),
|
|
172
24
|
});
|
|
173
25
|
}
|
|
174
|
-
|
|
175
26
|
app.synth();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
main().catch((error: unknown) => {
|
|
27
|
+
} catch (error) {
|
|
179
28
|
console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
|
|
180
29
|
process.exit(1);
|
|
181
|
-
}
|
|
30
|
+
}
|
|
@@ -1,94 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
AgentCoreMcp,
|
|
4
|
-
AgentCorePayments,
|
|
5
|
-
type AgentCoreProjectSpec,
|
|
6
|
-
type AgentCoreMcpSpec,
|
|
7
|
-
type HarnessDeploymentConfig,
|
|
8
|
-
} from '@aws/agentcore-cdk';
|
|
9
|
-
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
|
|
1
|
+
import { AgentCoreApplication, type AgentCoreApplicationProps } from '@aws/agentcore-cdk';
|
|
2
|
+
import { Stack, type StackProps } from 'aws-cdk-lib';
|
|
10
3
|
import { Construct } from 'constructs';
|
|
11
4
|
|
|
12
|
-
/**
|
|
13
|
-
* Harness deployment config: role-scoped fields (for IAM role + container build)
|
|
14
|
-
* plus the full validated spec + its config directory so the L3 construct can
|
|
15
|
-
* synthesize the AWS::BedrockAgentCore::Harness resource.
|
|
16
|
-
*/
|
|
17
|
-
export type HarnessConfig = HarnessDeploymentConfig;
|
|
18
|
-
|
|
19
5
|
export interface AgentCoreStackProps extends StackProps {
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
*/
|
|
23
|
-
spec: AgentCoreProjectSpec;
|
|
24
|
-
/**
|
|
25
|
-
* The MCP specification containing gateways and servers.
|
|
26
|
-
*/
|
|
27
|
-
mcpSpec?: AgentCoreMcpSpec;
|
|
28
|
-
/**
|
|
29
|
-
* Credential provider ARNs from deployed state, keyed by credential name.
|
|
30
|
-
*/
|
|
31
|
-
credentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string }>;
|
|
32
|
-
/**
|
|
33
|
-
* Harness role configurations.
|
|
34
|
-
*/
|
|
35
|
-
harnesses?: HarnessConfig[];
|
|
36
|
-
/**
|
|
37
|
-
* Parsed connectorParameters for non-S3 KB data sources, keyed by
|
|
38
|
-
* connectorConfigFile path. Forwarded to AgentCoreApplication.
|
|
39
|
-
*/
|
|
40
|
-
connectorParametersByFile?: Record<string, Record<string, unknown>>;
|
|
6
|
+
/** Props for the AgentCore application, produced by transformAgentCoreJson from agentcore/agentcore.json. */
|
|
7
|
+
application: AgentCoreApplicationProps;
|
|
41
8
|
}
|
|
42
9
|
|
|
43
10
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* All resource logic and outputs are contained within the L3 constructs.
|
|
11
|
+
* The stack the AgentCore CLI deploys. Everything declared in agentcore/agentcore.json is
|
|
12
|
+
* created by the AgentCoreApplication construct. Add your own resources below it and wire
|
|
13
|
+
* them to your runtimes and harnesses through the application's accessors.
|
|
48
14
|
*/
|
|
49
15
|
export class AgentCoreStack extends Stack {
|
|
50
|
-
/** The AgentCore application containing all agent environments */
|
|
51
16
|
public readonly application: AgentCoreApplication;
|
|
52
17
|
|
|
53
18
|
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
|
|
54
19
|
super(scope, id, props);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
if (credentials) {
|
|
68
|
-
appProps.credentials = credentials;
|
|
69
|
-
}
|
|
70
|
-
this.application = new AgentCoreApplication(this, 'Application', appProps as any);
|
|
71
|
-
new AgentCorePayments(this, 'Payments', {
|
|
72
|
-
spec,
|
|
73
|
-
credentials,
|
|
74
|
-
agentCoreApplication: this.application,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Create AgentCoreMcp if there are gateways configured
|
|
78
|
-
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
|
|
79
|
-
new AgentCoreMcp(this, 'Mcp', {
|
|
80
|
-
projectName: spec.name,
|
|
81
|
-
mcpSpec,
|
|
82
|
-
agentCoreApplication: this.application,
|
|
83
|
-
credentials,
|
|
84
|
-
projectTags: spec.tags,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Stack-level output
|
|
89
|
-
new CfnOutput(this, 'StackNameOutput', {
|
|
90
|
-
description: 'Name of the CloudFormation Stack',
|
|
91
|
-
value: this.stackName,
|
|
92
|
-
});
|
|
20
|
+
this.application = new AgentCoreApplication(this, 'Application', props.application);
|
|
21
|
+
|
|
22
|
+
// Example: give a runtime a DynamoDB table.
|
|
23
|
+
//
|
|
24
|
+
// import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
25
|
+
// const orders = new dynamodb.Table(this, 'Orders', {
|
|
26
|
+
// partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
27
|
+
// });
|
|
28
|
+
// const checkout = this.application.runtime('checkout'); // or .harness('support')
|
|
29
|
+
// checkout.grantReadWrite(orders);
|
|
30
|
+
// checkout.addEnvironmentVariable('ORDERS_TABLE', orders.tableName);
|
|
93
31
|
}
|
|
94
32
|
}
|
|
@@ -7,24 +7,17 @@
|
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"watch": "tsc -w",
|
|
10
|
-
"test": "jest",
|
|
11
10
|
"cdk": "npm run build && cdk",
|
|
12
|
-
"clean": "rm -rf dist"
|
|
13
|
-
"format": "prettier --write .",
|
|
14
|
-
"format:check": "prettier --check ."
|
|
11
|
+
"clean": "rm -rf dist"
|
|
15
12
|
},
|
|
16
13
|
"devDependencies": {
|
|
17
|
-
"@types/jest": "~29.5.14",
|
|
18
14
|
"@types/node": "~24.13.3",
|
|
19
|
-
"jest": "~29.7.0",
|
|
20
|
-
"ts-jest": "~29.4.11",
|
|
21
15
|
"aws-cdk": "~2.1126.0",
|
|
22
|
-
"prettier": "~3.9.5",
|
|
23
16
|
"typescript": "~5.9.3"
|
|
24
17
|
},
|
|
25
18
|
"dependencies": {
|
|
26
|
-
"@aws/agentcore-cdk": "
|
|
27
|
-
"aws-cdk-lib": "~2.
|
|
19
|
+
"@aws/agentcore-cdk": "1.0.0-rc.2",
|
|
20
|
+
"aws-cdk-lib": "~2.269.0",
|
|
28
21
|
"constructs": "~10.7.0"
|
|
29
22
|
}
|
|
30
23
|
}
|
|
@@ -13,7 +13,11 @@ with your own logic.
|
|
|
13
13
|
- `pyproject.toml` — Python dependencies, managed with
|
|
14
14
|
[uv](https://docs.astral.sh/uv/).
|
|
15
15
|
- `execution-role-policy.json` — extra IAM the evaluator Lambda gets at runtime.
|
|
16
|
-
Add statements here for anything your logic calls (DynamoDB, S3, …).
|
|
16
|
+
Add statements here for anything your logic calls (Bedrock, DynamoDB, S3, …).
|
|
17
|
+
|
|
18
|
+
Third-party evaluation libraries are ordinary application dependencies. Add
|
|
19
|
+
them to `pyproject.toml`, configure them in `lambda_function.py`, and grant any
|
|
20
|
+
required permissions in `execution-role-policy.json`.
|
|
17
21
|
|
|
18
22
|
## Write your evaluator
|
|
19
23
|
|
|
@@ -10,7 +10,7 @@ readme = "README.md"
|
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"a2a-sdk[all] ~= 0.3.0",
|
|
13
|
-
"aws-opentelemetry-distro ~= 0.
|
|
13
|
+
"aws-opentelemetry-distro ~= 0.18.0",
|
|
14
14
|
"bedrock-agentcore[a2a] ~= 1.9.1",
|
|
15
15
|
"botocore[crt] ~= 1.43.0",
|
|
16
16
|
"strands-agents ~= 1.15.0",
|
|
@@ -7,9 +7,10 @@ session, and streams its response.
|
|
|
7
7
|
## What's here
|
|
8
8
|
|
|
9
9
|
- `main.py`: the agent. A `BedrockAgentCoreApp` wraps an async entrypoint
|
|
10
|
-
that reads `prompt` from the payload, runs the agent, and streams each
|
|
11
|
-
|
|
12
|
-
keyed on the Runtime session id carries history
|
|
10
|
+
that reads `prompt` from the payload, runs the agent, and streams each
|
|
11
|
+
completed model message back as a JSON event of content blocks. An
|
|
12
|
+
`InMemorySaver` checkpointer keyed on the Runtime session id carries history
|
|
13
|
+
between turns.
|
|
13
14
|
- `model/load.py`: creates the Bedrock chat model with `init_chat_model`.
|
|
14
15
|
- `pyproject.toml`: Python dependencies, managed with
|
|
15
16
|
[uv](https://docs.astral.sh/uv/). `agentcore project create` has already run
|
|
@@ -21,7 +22,7 @@ Run the agent locally from the project root:
|
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
agentcore project dev
|
|
24
|
-
agentcore project invoke runtime --local --payload '{"prompt":"What is 2 plus 3?"}'
|
|
25
|
+
agentcore project invoke runtime --local --name {{name}} --payload '{"prompt":"What is 2 plus 3?"}'
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
Environment variables for local development go in `agentcore/.env.local`
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from langchain.agents import create_agent
|
|
2
2
|
from langchain.tools import tool
|
|
3
|
-
from langchain_core.messages import AIMessageChunk
|
|
4
3
|
from langgraph.checkpoint.memory import InMemorySaver
|
|
5
4
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
6
5
|
from model.load import load_model
|
|
@@ -36,18 +35,15 @@ async def invoke(payload, context):
|
|
|
36
35
|
session_id = context.session_id or "default-session"
|
|
37
36
|
config = {"configurable": {"thread_id": session_id}}
|
|
38
37
|
|
|
39
|
-
async
|
|
38
|
+
async with await agent.astream_events(
|
|
40
39
|
{"messages": [{"role": "user", "content": prompt}]},
|
|
41
40
|
config,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
blocks = message.content_blocks
|
|
49
|
-
if blocks:
|
|
50
|
-
yield {"node": metadata["langgraph_node"], "content": blocks}
|
|
41
|
+
version="v3",
|
|
42
|
+
) as stream:
|
|
43
|
+
async for message in stream.messages:
|
|
44
|
+
blocks = (await message.output).content_blocks
|
|
45
|
+
if blocks:
|
|
46
|
+
yield {"node": message.node, "content": blocks}
|
|
51
47
|
|
|
52
48
|
|
|
53
49
|
if __name__ == "__main__":
|
|
@@ -16,8 +16,22 @@ file defines a Starlette ASGI app with the Strands Agent SDK running within.
|
|
|
16
16
|
## Input Validation
|
|
17
17
|
|
|
18
18
|
Validate invocation input before forwarding it to Strands. Keep plain prompts typed as strings. If the app accepts a
|
|
19
|
-
caller-supplied message history, retain `
|
|
20
|
-
invoking the agent.
|
|
19
|
+
caller-supplied message history, retain `_strip_trailing_tool_use()` in `parse.py`, which normalizes the history tail
|
|
20
|
+
before invoking the agent.
|
|
21
|
+
|
|
22
|
+
## Payload
|
|
23
|
+
|
|
24
|
+
The Runtime accepts a JSON object. `parse_payload()` reads:
|
|
25
|
+
|
|
26
|
+
- `prompt` (string) — a single user message. Used when `messages` is absent; defaults to `""`.
|
|
27
|
+
- `messages` (array) — a full conversation history (`[{"role": ..., "content": [...]}]`). Takes precedence over `prompt`; trailing `toolUse` blocks are stripped before the agent runs.
|
|
28
|
+
- `actorId` (string, optional) — identifies the end user for Memory scoping (e.g. `/users/{actorId}/facts`). Defaults to `"default"`.
|
|
29
|
+
|
|
30
|
+
Provide either `prompt` or `messages`. The `session_id` is not in the body — it comes from the `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` header (`context.session_id`); reuse it to continue a conversation.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "prompt": "What's the weather?", "actorId": "user-123" }
|
|
34
|
+
```
|
|
21
35
|
|
|
22
36
|
## Environment Variables
|
|
23
37
|
|
|
@@ -1,106 +1,65 @@
|
|
|
1
|
-
from
|
|
1
|
+
from functools import lru_cache
|
|
2
2
|
|
|
3
3
|
from strands import Agent, tool
|
|
4
4
|
from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
|
|
5
5
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
6
6
|
from model.load import load_model
|
|
7
|
+
from parse import parse_payload
|
|
7
8
|
from memory.session import get_memory_session_manager
|
|
8
9
|
|
|
9
|
-
app = BedrockAgentCoreApp()
|
|
10
|
-
log = app.logger
|
|
11
|
-
|
|
12
10
|
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant. Use tools when appropriate."
|
|
13
11
|
|
|
14
12
|
# Define a collection of tools used by the model
|
|
15
13
|
tools = []
|
|
16
14
|
|
|
17
|
-
|
|
18
15
|
# Define a simple function tool
|
|
19
16
|
@tool
|
|
20
17
|
def add_numbers(a: int, b: int) -> int:
|
|
21
18
|
"""Return the sum of two numbers"""
|
|
22
19
|
return a + b
|
|
23
20
|
|
|
24
|
-
|
|
25
21
|
tools.append(add_numbers)
|
|
26
22
|
|
|
27
|
-
|
|
28
23
|
def _make_conversation_manager():
|
|
29
24
|
return NullConversationManager()
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
messages.pop()
|
|
70
|
-
|
|
71
|
-
return messages
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _extract_prompt(payload: dict):
|
|
75
|
-
"""Accept a caller-supplied message history or a plain prompt string."""
|
|
76
|
-
if not isinstance(payload, dict):
|
|
77
|
-
raise ValueError("payload must be a JSON object")
|
|
78
|
-
if "messages" in payload:
|
|
79
|
-
return strip_trailing_tool_use(payload["messages"])
|
|
80
|
-
prompt = payload.get("prompt", "")
|
|
81
|
-
if not isinstance(prompt, str):
|
|
82
|
-
raise ValueError("prompt must be a string")
|
|
83
|
-
return prompt
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@app.entrypoint
|
|
87
|
-
async def invoke(payload, context):
|
|
88
|
-
log.info("Invoking Agent.....")
|
|
89
|
-
|
|
90
|
-
session_id = getattr(context, "session_id", "default-session")
|
|
91
|
-
user_id = getattr(context, "user_id", "default-user")
|
|
92
|
-
agent = get_or_create_agent(session_id, user_id)
|
|
93
|
-
|
|
94
|
-
prompt = _extract_prompt(payload)
|
|
95
|
-
|
|
96
|
-
async for event in agent.stream_async(prompt):
|
|
97
|
-
if not isinstance(event, dict) or "event" not in event:
|
|
98
|
-
continue
|
|
99
|
-
cbs = event["event"].get("contentBlockStart")
|
|
100
|
-
if cbs is not None and not cbs.get("start"):
|
|
101
|
-
continue
|
|
102
|
-
yield event
|
|
103
|
-
|
|
104
|
-
|
|
26
|
+
@lru_cache(maxsize=128)
|
|
27
|
+
def _get_agent(session_id: str, actor_id: str) -> Agent:
|
|
28
|
+
"""Given a session_id and actor_id, return or construct the corresponding Strands agent.
|
|
29
|
+
Note: caching helps avoid repeated identity fetches on non-bedrock model loads
|
|
30
|
+
and supports in-memory session management for local dev."""
|
|
31
|
+
return Agent(
|
|
32
|
+
model=load_model(),
|
|
33
|
+
session_manager=get_memory_session_manager(session_id, actor_id),
|
|
34
|
+
conversation_manager=_make_conversation_manager(),
|
|
35
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
|
36
|
+
tools=tools,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def create_app():
|
|
40
|
+
app = BedrockAgentCoreApp()
|
|
41
|
+
log = app.logger
|
|
42
|
+
|
|
43
|
+
@app.entrypoint
|
|
44
|
+
async def invoke(payload, context):
|
|
45
|
+
log.info("Invoking Agent.....")
|
|
46
|
+
|
|
47
|
+
session_id = getattr(context, "session_id", None) or "default-session"
|
|
48
|
+
prompt, actor_id = parse_payload(payload)
|
|
49
|
+
|
|
50
|
+
log.info(f"Invoking with session_id={session_id} and actor_id={actor_id}")
|
|
51
|
+
agent = _get_agent(session_id, actor_id)
|
|
52
|
+
|
|
53
|
+
async for event in agent.stream_async(prompt):
|
|
54
|
+
if not isinstance(event, dict) or "event" not in event:
|
|
55
|
+
continue
|
|
56
|
+
cbs = event["event"].get("contentBlockStart")
|
|
57
|
+
if cbs is not None and not cbs.get("start"):
|
|
58
|
+
continue
|
|
59
|
+
yield event
|
|
60
|
+
|
|
61
|
+
return app
|
|
62
|
+
|
|
63
|
+
app = create_app()
|
|
105
64
|
if __name__ == "__main__":
|
|
106
65
|
app.run()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import uuid
|
|
3
2
|
from typing import Optional
|
|
4
3
|
|
|
5
4
|
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig
|
|
@@ -8,15 +7,12 @@ from bedrock_agentcore.memory.integrations.strands.session_manager import AgentC
|
|
|
8
7
|
MEMORY_ID = os.getenv("{{memoryEnvVarName}}")
|
|
9
8
|
REGION = os.getenv("AWS_REGION")
|
|
10
9
|
|
|
11
|
-
|
|
12
10
|
def get_memory_session_manager(
|
|
13
|
-
|
|
11
|
+
session_id: str, actor_id: str,
|
|
14
12
|
) -> Optional[AgentCoreMemorySessionManager]:
|
|
15
13
|
if not MEMORY_ID:
|
|
16
14
|
return None
|
|
17
15
|
|
|
18
|
-
session_id = session_id or uuid.uuid4().hex
|
|
19
|
-
|
|
20
16
|
retrieval_config = {
|
|
21
17
|
f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
22
18
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|