@aws/nx-plugin 1.2.0 → 1.3.0

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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/generators.json +8 -0
  3. package/migrations.json +6 -1
  4. package/package.json +1 -1
  5. package/src/internal/test-matrix/generator.js +6 -1
  6. package/src/internal/test-matrix/generator.js.map +1 -1
  7. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/metadata.json +3 -0
  8. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
  9. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
  10. package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
  11. package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
  12. package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
  13. package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
  14. package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
  15. package/src/py/agent/guardrails/generator.d.ts +10 -0
  16. package/src/py/agent/guardrails/generator.js +175 -0
  17. package/src/py/agent/guardrails/generator.js.map +1 -0
  18. package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
  19. package/src/py/agent/guardrails/schema.d.js +6 -0
  20. package/src/py/agent/guardrails/schema.d.js.map +1 -0
  21. package/src/py/agent/guardrails/schema.d.ts +9 -0
  22. package/src/py/agent/guardrails/schema.json +25 -0
  23. package/src/sdk/py.d.ts +2 -0
  24. package/src/sdk/py.js +1 -0
  25. package/src/sdk/py.js.map +1 -1
  26. package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +54 -3
  27. package/src/ts/agent/guardrails/generator.js +2 -1
  28. package/src/ts/agent/guardrails/generator.js.map +1 -1
  29. package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
  30. package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
  31. package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
  32. package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
  33. package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
  34. package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
  35. package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
  36. package/src/utils/test/fake-aws/fake_aws.py +205 -0
  37. package/src/utils/test/python-dependencies.d.ts +6 -0
  38. package/src/utils/test/python-dependencies.js +19 -0
  39. package/src/utils/test/python-dependencies.js.map +1 -1
  40. package/src/utils/test/warm-python-cache.js +5 -1
  41. package/src/utils/test/warm-python-cache.js.map +1 -1
  42. /package/src/{ts/agent/guardrails/files/agent → utils/guardrail-constructs/files/policy}/guardrail.json.template +0 -0
@@ -0,0 +1,135 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ import threading
5
+ import time
6
+ from collections.abc import Iterator
7
+ from typing import Any, TypedDict
8
+
9
+ from langchain_aws import ChatBedrockConverse
10
+ from langchain_core.callbacks import CallbackManagerForLLMRun
11
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
12
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
13
+
14
+ from <%- agentConnectionModuleName %>.core.runtime_config import get_agentcore_runtime_config
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.<%- agentNameClassName %>"
19
+ CACHE_SECONDS = 5
20
+ REDACTED_INPUT = "[User input redacted.]"
21
+
22
+
23
+ class GuardrailConfig(TypedDict):
24
+ guardrailIdentifier: str
25
+ guardrailVersion: str
26
+ trace: str
27
+
28
+
29
+ _lock = threading.Lock()
30
+ _cache: tuple[float, dict[str, Any] | None] | None = None
31
+ _warned_unguarded = False
32
+
33
+
34
+ def _read_entry() -> dict[str, Any] | None:
35
+ """This agent's entry in runtime config, written by the <%- agentNameClassName %>Guardrail
36
+ construct or the <%- nameKebabCase %> guardrail module, cached for CACHE_SECONDS."""
37
+ global _cache
38
+ with _lock:
39
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
40
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
41
+ _cache = (time.monotonic(), guardrails.get("<%- agentNameClassName %>"))
42
+ return _cache[1]
43
+
44
+
45
+ def resolve_guardrail() -> GuardrailConfig | None:
46
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
47
+ global _warned_unguarded
48
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
49
+ entry: dict[str, Any] | None = None
50
+ if not local_dev:
51
+ entry = _read_entry()
52
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
53
+ try:
54
+ entry = _read_entry()
55
+ except Exception:
56
+ entry = None
57
+
58
+ guardrail_id = (entry or {}).get("guardrailId")
59
+ guardrail_version = (entry or {}).get("guardrailVersion")
60
+ if not guardrail_id or not guardrail_version:
61
+ if not local_dev:
62
+ raise RuntimeError(
63
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
64
+ "Add the <%- agentNameClassName %>Guardrail construct (CDK) or the "
65
+ "guardrails/<%- nameKebabCase %> module (Terraform) to your infrastructure."
66
+ )
67
+ if not _warned_unguarded:
68
+ _warned_unguarded = True
69
+ logger.warning(
70
+ "No guardrail for this agent in runtime config at %s, "
71
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
72
+ RUNTIME_CONFIG_KEY,
73
+ )
74
+ return None
75
+
76
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
77
+ raise RuntimeError(
78
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
79
+ f"must be a numbered guardrail version, but is {guardrail_version}."
80
+ )
81
+ return {"guardrailIdentifier": str(guardrail_id), "guardrailVersion": str(guardrail_version), "trace": "enabled"}
82
+
83
+
84
+ def _blocked(value: Any) -> bool:
85
+ """Whether a guardrail trace holds a policy that detected and blocked content."""
86
+ if isinstance(value, dict):
87
+ if value.get("action") == "BLOCKED" and value.get("detected") is True:
88
+ return True
89
+ return any(_blocked(item) for item in value.values())
90
+ if isinstance(value, list):
91
+ return any(_blocked(item) for item in value)
92
+ return False
93
+
94
+
95
+ def redact_blocked_input(messages: list[BaseMessage]) -> list[BaseMessage]:
96
+ """The messages with each user message whose turn the guardrail blocked replaced by
97
+ REDACTED_INPUT, as Strands does. Bedrock checks every message of the conversation on
98
+ each call, so a blocked message left in the session would block every later message."""
99
+ redacted = list(messages)
100
+ for index, message in enumerate(messages):
101
+ if not isinstance(message, AIMessage) or message.response_metadata.get("stopReason") != "guardrail_intervened":
102
+ continue
103
+ trace = (message.response_metadata.get("trace") or {}).get("guardrail")
104
+ if trace is not None and not _blocked(trace):
105
+ continue
106
+ for previous in range(index - 1, -1, -1):
107
+ if isinstance(redacted[previous], HumanMessage):
108
+ redacted[previous] = redacted[previous].model_copy(update={"content": REDACTED_INPUT})
109
+ break
110
+ return redacted
111
+
112
+
113
+ class GuardrailedChatBedrockConverse(ChatBedrockConverse):
114
+ """A ChatBedrockConverse model that applies this agent's guardrail from runtime config
115
+ to each model call, so a new guardrail version applies without a restart."""
116
+
117
+ def _generate(
118
+ self,
119
+ messages: list[BaseMessage],
120
+ stop: list[str] | None = None,
121
+ run_manager: CallbackManagerForLLMRun | None = None,
122
+ **kwargs: Any,
123
+ ) -> ChatResult:
124
+ kwargs["guardrail_config"] = resolve_guardrail()
125
+ return super()._generate(redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs)
126
+
127
+ def _stream(
128
+ self,
129
+ messages: list[BaseMessage],
130
+ stop: list[str] | None = None,
131
+ run_manager: CallbackManagerForLLMRun | None = None,
132
+ **kwargs: Any,
133
+ ) -> Iterator[ChatGenerationChunk]:
134
+ kwargs["guardrail_config"] = resolve_guardrail()
135
+ yield from super()._stream(redact_blocked_input(messages), stop=stop, run_manager=run_manager, **kwargs)
@@ -0,0 +1,93 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ import threading
5
+ import time
6
+ from typing import Any, TypedDict
7
+
8
+ from strands.models import BedrockModel
9
+
10
+ from <%- agentConnectionModuleName %>.core.runtime_config import get_agentcore_runtime_config
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ RUNTIME_CONFIG_KEY = "agentcore.guardrails.<%- agentNameClassName %>"
15
+ CACHE_SECONDS = 5
16
+
17
+
18
+ class GuardrailConfig(TypedDict):
19
+ guardrailIdentifier: str
20
+ guardrailVersion: str
21
+ trace: str
22
+
23
+
24
+ _lock = threading.Lock()
25
+ _cache: tuple[float, dict[str, Any] | None] | None = None
26
+ _warned_unguarded = False
27
+
28
+
29
+ def _read_entry() -> dict[str, Any] | None:
30
+ """This agent's entry in runtime config, written by the <%- agentNameClassName %>Guardrail
31
+ construct or the <%- nameKebabCase %> guardrail module, cached for CACHE_SECONDS."""
32
+ global _cache
33
+ with _lock:
34
+ if _cache is None or time.monotonic() - _cache[0] >= CACHE_SECONDS:
35
+ guardrails = get_agentcore_runtime_config().get("guardrails") or {}
36
+ _cache = (time.monotonic(), guardrails.get("<%- agentNameClassName %>"))
37
+ return _cache[1]
38
+
39
+
40
+ def resolve_guardrail() -> GuardrailConfig | None:
41
+ """The guardrail to apply, read from runtime config on every model call (cached for 5 seconds)."""
42
+ global _warned_unguarded
43
+ local_dev = os.environ.get("LOCAL_DEV") == "true"
44
+ entry: dict[str, Any] | None = None
45
+ if not local_dev:
46
+ entry = _read_entry()
47
+ elif os.environ.get("RUNTIME_CONFIG_APP_ID"):
48
+ try:
49
+ entry = _read_entry()
50
+ except Exception:
51
+ entry = None
52
+
53
+ guardrail_id = (entry or {}).get("guardrailId")
54
+ guardrail_version = (entry or {}).get("guardrailVersion")
55
+ if not guardrail_id or not guardrail_version:
56
+ if not local_dev:
57
+ raise RuntimeError(
58
+ f"No guardrail for this agent in runtime config at {RUNTIME_CONFIG_KEY}. "
59
+ "Add the <%- agentNameClassName %>Guardrail construct (CDK) or the "
60
+ "guardrails/<%- nameKebabCase %> module (Terraform) to your infrastructure."
61
+ )
62
+ if not _warned_unguarded:
63
+ _warned_unguarded = True
64
+ logger.warning(
65
+ "No guardrail for this agent in runtime config at %s, "
66
+ "so the agent is running without a guardrail (LOCAL_DEV=true).",
67
+ RUNTIME_CONFIG_KEY,
68
+ )
69
+ return None
70
+
71
+ if not re.fullmatch(r"[1-9][0-9]*", str(guardrail_version)):
72
+ raise RuntimeError(
73
+ f"The guardrail version in runtime config at {RUNTIME_CONFIG_KEY} "
74
+ f"must be a numbered guardrail version, but is {guardrail_version}."
75
+ )
76
+ return {"guardrailIdentifier": str(guardrail_id), "guardrailVersion": str(guardrail_version), "trace": "enabled"}
77
+
78
+
79
+ class GuardrailedBedrockModel(BedrockModel):
80
+ """A Bedrock model that applies this agent's guardrail from runtime config to each
81
+ model call, so a new guardrail version applies without a restart."""
82
+
83
+ def format_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
84
+ request = super().format_request(*args, **kwargs)
85
+ guardrail = resolve_guardrail()
86
+ if guardrail is not None:
87
+ request["guardrailConfig"] = guardrail
88
+ return request
89
+
90
+
91
+ def get_guardrailed_model() -> BedrockModel:
92
+ """The Bedrock model with this agent's guardrail applied."""
93
+ return GuardrailedBedrockModel()
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { type GeneratorCallback, type Tree } from '@nx/devkit';
6
+ import { type NxGeneratorInfo } from '../../../utils/nx.js';
7
+ import type { PyAgentGuardrailsGeneratorSchema } from './schema';
8
+ export declare const PY_AGENT_GUARDRAILS_GENERATOR_INFO: NxGeneratorInfo;
9
+ export declare const pyAgentGuardrailsGenerator: (tree: Tree, options: PyAgentGuardrailsGeneratorSchema) => Promise<GeneratorCallback>;
10
+ export default pyAgentGuardrailsGenerator;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { generateFiles, joinPathFragments, logger, OverwriteStrategy } from "@nx/devkit";
5
+ import { addGuardrailPolicySynthInputs } from "../../../ts/agent/guardrails/synth-inputs.js";
6
+ import { getPythonAgentConnectionModuleName } from "../../../utils/agent-connection/agent-connection.js";
7
+ import { addPythonDestructuredImport, applyGritQL, matchGritQL } from "../../../utils/ast.js";
8
+ import { formatFilesInSubtree } from "../../../utils/format.js";
9
+ import { addGuardrailInfra, addGuardrailPolicy } from "../../../utils/guardrail-constructs/guardrail-constructs.js";
10
+ import { guardrailWiringMessage, PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL } from "../../../utils/guardrail-constructs/guardrail-wiring.js";
11
+ import { addGeneratorMetricsIfApplicable } from "../../../utils/metrics.js";
12
+ import { kebabCase, toClassName } from "../../../utils/names.js";
13
+ import { getNpmScope } from "../../../utils/npm-scope.js";
14
+ import { addComponentGeneratorMetadata, getGeneratorInfo, readProjectConfigurationUnqualified } from "../../../utils/nx.js";
15
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../../../utils/shared-constructs-constants.js";
16
+ import { PY_AGENT_GENERATOR_INFO } from "../generator.js";
17
+ export const PY_AGENT_GUARDRAILS_GENERATOR_INFO = getGeneratorInfo(import.meta.filename);
18
+ /**
19
+ * Find the `py#agent` component to guard, by name when given, otherwise the
20
+ * project's only one.
21
+ */ const resolveAgent = (projectName, components, agent)=>{
22
+ const agents = components.filter((c)=>c.generator === PY_AGENT_GENERATOR_INFO.id);
23
+ const available = agents.map((c)=>c.name).join(', ') || 'none';
24
+ if (agent) {
25
+ const match = agents.find((c)=>c.name === agent);
26
+ if (!match) {
27
+ throw new Error(`No Python Agent named ${agent} in project ${projectName}. Available agents: ${available}`);
28
+ }
29
+ return match;
30
+ }
31
+ if (agents.length !== 1) {
32
+ throw new Error(agents.length === 0 ? `Project ${projectName} has no Python Agent. Add one with the py#agent generator first.` : `Project ${projectName} has more than one Python Agent. Specify which to guard with --agent. Available agents: ${available}`);
33
+ }
34
+ return agents[0];
35
+ };
36
+ /**
37
+ * The guardrail construct or module is keyed by the agent's class name, so a
38
+ * TypeScript agent with the same class name already owns it. Refuse rather
39
+ * than leave this agent unguarded behind the other agent's policy.
40
+ */ const assertNoOtherAgentGuardrail = (tree, iac, nameClassName, nameKebabCase, policyPath)=>{
41
+ const file = iac === 'cdk' ? joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, 'src', 'app', 'guardrails', nameKebabCase, `${nameKebabCase}-guardrail.ts`) : joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', 'app', 'guardrails', nameKebabCase, `${nameKebabCase}-guardrail.tf`);
42
+ if (!tree.exists(file)) {
43
+ return;
44
+ }
45
+ const contents = tree.read(file, 'utf-8');
46
+ if ([
47
+ `'${policyPath}'`,
48
+ `"${policyPath}"`,
49
+ `/${policyPath}"`
50
+ ].some((ref)=>contents.includes(ref))) {
51
+ return;
52
+ }
53
+ throw new Error(`Guardrail infrastructure for ${nameClassName} already exists at ${file} for another agent's guardrail.json. Agents in different projects that share the class name ${nameClassName} share one runtime construct; give one of them a different name.`);
54
+ };
55
+ /**
56
+ * Add `model=get_guardrailed_model()` to the `Agent(...)` call in a Strands
57
+ * agent.py. An agent that already sets its own model is left unchanged, and
58
+ * the lines to add by hand are logged instead.
59
+ */ const addGuardrailedModelToStrandsAgent = async (tree, agentFile)=>{
60
+ if (tree.exists(agentFile)) {
61
+ if (await matchGritQL(tree, agentFile, 'language python\n`get_guardrailed_model()`')) {
62
+ return;
63
+ }
64
+ if (await applyGritQL(tree, agentFile, 'language python\n`Agent($args)` => `Agent(model=get_guardrailed_model(), $args)` where { $args <: not contains `model=$_` }')) {
65
+ await addPythonDestructuredImport(tree, agentFile, [
66
+ 'get_guardrailed_model'
67
+ ], '.guardrail');
68
+ return;
69
+ }
70
+ }
71
+ logger.warn(`Could not add the guardrail to ${agentFile}, because ${tree.exists(agentFile) ? 'its Agent(...) call already sets a model or was not found' : 'the file does not exist'}. Set the model of your Agent to the guardrailed model by hand:
72
+
73
+ model=get_guardrailed_model(),
74
+
75
+ and import it:
76
+
77
+ from .guardrail import get_guardrailed_model`);
78
+ };
79
+ /**
80
+ * Replace the `ChatBedrockConverse(...)` call in a LangChain agent.py with the
81
+ * guardrailed subclass, keeping its arguments. An agent that builds its model
82
+ * another way is left unchanged, and the lines to add by hand are logged.
83
+ */ const addGuardrailedModelToLangChainAgent = async (tree, agentFile)=>{
84
+ if (tree.exists(agentFile)) {
85
+ if (await matchGritQL(tree, agentFile, 'language python\n`GuardrailedChatBedrockConverse($_)`')) {
86
+ return;
87
+ }
88
+ if (await applyGritQL(tree, agentFile, 'language python\n`ChatBedrockConverse($args)` => `GuardrailedChatBedrockConverse($args)`')) {
89
+ await addPythonDestructuredImport(tree, agentFile, [
90
+ 'GuardrailedChatBedrockConverse'
91
+ ], '.guardrail');
92
+ return;
93
+ }
94
+ }
95
+ logger.warn(`Could not add the guardrail to ${agentFile}, because ${tree.exists(agentFile) ? 'it has no ChatBedrockConverse(...) call' : 'the file does not exist'}. Create your model with the guardrailed class by hand:
96
+
97
+ model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)
98
+
99
+ and import it:
100
+
101
+ from .guardrail import GuardrailedChatBedrockConverse`);
102
+ };
103
+ export const pyAgentGuardrailsGenerator = async (tree, options)=>{
104
+ const project = readProjectConfigurationUnqualified(tree, options.project);
105
+ const agent = resolveAgent(project.name, project.metadata?.components ?? [], options.agent);
106
+ const agentTargetPrefix = agent.name ?? 'agent';
107
+ const agentNameClassName = agent.rc ?? toClassName(agentTargetPrefix);
108
+ const nameKebabCase = kebabCase(agentNameClassName);
109
+ const framework = agent.framework ?? 'strands';
110
+ const moduleName = (project.sourceRoot ?? project.root).split('/').pop();
111
+ const agentPath = joinPathFragments(agent.path ?? joinPathFragments(moduleName, 'agent'));
112
+ const agentDir = joinPathFragments(project.root, agentPath);
113
+ const policyPath = joinPathFragments(agentDir, 'guardrail.json');
114
+ const iac = agent.iac;
115
+ if (iac) {
116
+ assertNoOtherAgentGuardrail(tree, iac, agentNameClassName, nameKebabCase, policyPath);
117
+ }
118
+ // The policy and guardrail.py are the user's to edit, so a re-run never
119
+ // touches them.
120
+ generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', framework), agentDir, {
121
+ agentConnectionModuleName: getPythonAgentConnectionModuleName(tree),
122
+ agentNameClassName,
123
+ nameKebabCase
124
+ }, {
125
+ overwriteStrategy: OverwriteStrategy.KeepExisting
126
+ });
127
+ addGuardrailPolicy(tree, agentDir);
128
+ const agentFile = joinPathFragments(agentDir, 'agent.py');
129
+ if (framework === 'langchain') {
130
+ await addGuardrailedModelToLangChainAgent(tree, agentFile);
131
+ } else {
132
+ await addGuardrailedModelToStrandsAgent(tree, agentFile);
133
+ }
134
+ let wiringMessage;
135
+ if (iac) {
136
+ await addGuardrailInfra(tree, {
137
+ iac,
138
+ nameClassName: agentNameClassName,
139
+ nameKebabCase,
140
+ policyPath
141
+ });
142
+ if (iac === 'cdk') {
143
+ for (const message of addGuardrailPolicySynthInputs(tree, [
144
+ policyPath
145
+ ])){
146
+ logger.warn(message);
147
+ }
148
+ }
149
+ wiringMessage = guardrailWiringMessage({
150
+ iac,
151
+ nameClassName: agentNameClassName,
152
+ nameKebabCase,
153
+ npmScope: getNpmScope(tree),
154
+ deployGuideUrl: PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL
155
+ });
156
+ } else {
157
+ logger.warn(`Agent ${agentTargetPrefix} in project ${project.name} has no infrastructure, so no guardrail infrastructure was generated. Create the guardrail yourself and write its ID and version to runtime config at agentcore.guardrails.${agentNameClassName} as { guardrailId, guardrailVersion }.`);
158
+ }
159
+ addComponentGeneratorMetadata(tree, project.name, PY_AGENT_GUARDRAILS_GENERATOR_INFO, agentPath, agentTargetPrefix);
160
+ await addGeneratorMetricsIfApplicable(tree, [
161
+ PY_AGENT_GUARDRAILS_GENERATOR_INFO
162
+ ]);
163
+ await formatFilesInSubtree(tree);
164
+ // Printed from the callback so it appears after Nx lists the changed
165
+ // files, on every run, since the generator cannot tell whether the stack is
166
+ // already wired.
167
+ return ()=>{
168
+ if (wiringMessage) {
169
+ logger.info(wiringMessage);
170
+ }
171
+ };
172
+ };
173
+ export default pyAgentGuardrailsGenerator;
174
+
175
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/py/agent/guardrails/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type GeneratorCallback,\n generateFiles,\n joinPathFragments,\n logger,\n OverwriteStrategy,\n type Tree,\n} from '@nx/devkit';\nimport { addGuardrailPolicySynthInputs } from '../../../ts/agent/guardrails/synth-inputs.js';\nimport { getPythonAgentConnectionModuleName } from '../../../utils/agent-connection/agent-connection.js';\nimport {\n addPythonDestructuredImport,\n applyGritQL,\n matchGritQL,\n} from '../../../utils/ast.js';\nimport { formatFilesInSubtree } from '../../../utils/format.js';\nimport {\n addGuardrailInfra,\n addGuardrailPolicy,\n} from '../../../utils/guardrail-constructs/guardrail-constructs.js';\nimport {\n guardrailWiringMessage,\n PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL,\n} from '../../../utils/guardrail-constructs/guardrail-wiring.js';\nimport type { Iac } from '../../../utils/iac.js';\nimport { addGeneratorMetricsIfApplicable } from '../../../utils/metrics.js';\nimport { kebabCase, toClassName } from '../../../utils/names.js';\nimport { getNpmScope } from '../../../utils/npm-scope.js';\nimport {\n addComponentGeneratorMetadata,\n type ComponentMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n readProjectConfigurationUnqualified,\n} from '../../../utils/nx.js';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants.js';\nimport { PY_AGENT_GENERATOR_INFO } from '../generator.js';\nimport type { PyAgentFramework } from '../schema';\nimport type { PyAgentGuardrailsGeneratorSchema } from './schema';\n\nexport const PY_AGENT_GUARDRAILS_GENERATOR_INFO: NxGeneratorInfo =\n getGeneratorInfo(import.meta.filename);\n\n/**\n * Find the `py#agent` component to guard, by name when given, otherwise the\n * project's only one.\n */\nconst resolveAgent = (\n projectName: string,\n components: ComponentMetadata[],\n agent?: string,\n): ComponentMetadata => {\n const agents = components.filter(\n (c) => c.generator === PY_AGENT_GENERATOR_INFO.id,\n );\n const available = agents.map((c) => c.name).join(', ') || 'none';\n if (agent) {\n const match = agents.find((c) => c.name === agent);\n if (!match) {\n throw new Error(\n `No Python Agent named ${agent} in project ${projectName}. Available agents: ${available}`,\n );\n }\n return match;\n }\n if (agents.length !== 1) {\n throw new Error(\n agents.length === 0\n ? `Project ${projectName} has no Python Agent. Add one with the py#agent generator first.`\n : `Project ${projectName} has more than one Python Agent. Specify which to guard with --agent. Available agents: ${available}`,\n );\n }\n return agents[0];\n};\n\n/**\n * The guardrail construct or module is keyed by the agent's class name, so a\n * TypeScript agent with the same class name already owns it. Refuse rather\n * than leave this agent unguarded behind the other agent's policy.\n */\nconst assertNoOtherAgentGuardrail = (\n tree: Tree,\n iac: Iac,\n nameClassName: string,\n nameKebabCase: string,\n policyPath: string,\n): void => {\n const file =\n iac === 'cdk'\n ? joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'guardrails',\n nameKebabCase,\n `${nameKebabCase}-guardrail.ts`,\n )\n : joinPathFragments(\n PACKAGES_DIR,\n SHARED_TERRAFORM_DIR,\n 'src',\n 'app',\n 'guardrails',\n nameKebabCase,\n `${nameKebabCase}-guardrail.tf`,\n );\n if (!tree.exists(file)) {\n return;\n }\n const contents = tree.read(file, 'utf-8');\n if (\n [`'${policyPath}'`, `\"${policyPath}\"`, `/${policyPath}\"`].some((ref) =>\n contents.includes(ref),\n )\n ) {\n return;\n }\n throw new Error(\n `Guardrail infrastructure for ${nameClassName} already exists at ${file} for another agent's guardrail.json. Agents in different projects that share the class name ${nameClassName} share one runtime construct; give one of them a different name.`,\n );\n};\n\n/**\n * Add `model=get_guardrailed_model()` to the `Agent(...)` call in a Strands\n * agent.py. An agent that already sets its own model is left unchanged, and\n * the lines to add by hand are logged instead.\n */\nconst addGuardrailedModelToStrandsAgent = async (\n tree: Tree,\n agentFile: string,\n): Promise<void> => {\n if (tree.exists(agentFile)) {\n if (\n await matchGritQL(\n tree,\n agentFile,\n 'language python\\n`get_guardrailed_model()`',\n )\n ) {\n return;\n }\n if (\n await applyGritQL(\n tree,\n agentFile,\n 'language python\\n`Agent($args)` => `Agent(model=get_guardrailed_model(), $args)` where { $args <: not contains `model=$_` }',\n )\n ) {\n await addPythonDestructuredImport(\n tree,\n agentFile,\n ['get_guardrailed_model'],\n '.guardrail',\n );\n return;\n }\n }\n\n logger.warn(\n `Could not add the guardrail to ${agentFile}, because ${tree.exists(agentFile) ? 'its Agent(...) call already sets a model or was not found' : 'the file does not exist'}. Set the model of your Agent to the guardrailed model by hand:\n\n model=get_guardrailed_model(),\n\nand import it:\n\n from .guardrail import get_guardrailed_model`,\n );\n};\n\n/**\n * Replace the `ChatBedrockConverse(...)` call in a LangChain agent.py with the\n * guardrailed subclass, keeping its arguments. An agent that builds its model\n * another way is left unchanged, and the lines to add by hand are logged.\n */\nconst addGuardrailedModelToLangChainAgent = async (\n tree: Tree,\n agentFile: string,\n): Promise<void> => {\n if (tree.exists(agentFile)) {\n if (\n await matchGritQL(\n tree,\n agentFile,\n 'language python\\n`GuardrailedChatBedrockConverse($_)`',\n )\n ) {\n return;\n }\n if (\n await applyGritQL(\n tree,\n agentFile,\n 'language python\\n`ChatBedrockConverse($args)` => `GuardrailedChatBedrockConverse($args)`',\n )\n ) {\n await addPythonDestructuredImport(\n tree,\n agentFile,\n ['GuardrailedChatBedrockConverse'],\n '.guardrail',\n );\n return;\n }\n }\n\n logger.warn(\n `Could not add the guardrail to ${agentFile}, because ${tree.exists(agentFile) ? 'it has no ChatBedrockConverse(...) call' : 'the file does not exist'}. Create your model with the guardrailed class by hand:\n\n model = GuardrailedChatBedrockConverse(model=MODEL_ID, region_name=REGION)\n\nand import it:\n\n from .guardrail import GuardrailedChatBedrockConverse`,\n );\n};\n\nexport const pyAgentGuardrailsGenerator = async (\n tree: Tree,\n options: PyAgentGuardrailsGeneratorSchema,\n): Promise<GeneratorCallback> => {\n const project = readProjectConfigurationUnqualified(tree, options.project);\n const agent = resolveAgent(\n project.name,\n (project.metadata as any)?.components ?? [],\n options.agent,\n );\n\n const agentTargetPrefix = agent.name ?? 'agent';\n const agentNameClassName = agent.rc ?? toClassName(agentTargetPrefix);\n const nameKebabCase = kebabCase(agentNameClassName);\n const framework: PyAgentFramework = agent.framework ?? 'strands';\n const moduleName = (project.sourceRoot ?? project.root).split('/').pop();\n const agentPath = joinPathFragments(\n agent.path ?? joinPathFragments(moduleName, 'agent'),\n );\n const agentDir = joinPathFragments(project.root, agentPath);\n const policyPath = joinPathFragments(agentDir, 'guardrail.json');\n const iac = agent.iac as Iac | undefined;\n\n if (iac) {\n assertNoOtherAgentGuardrail(\n tree,\n iac,\n agentNameClassName,\n nameKebabCase,\n policyPath,\n );\n }\n\n // The policy and guardrail.py are the user's to edit, so a re-run never\n // touches them.\n generateFiles(\n tree,\n joinPathFragments(import.meta.dirname, 'files', framework),\n agentDir,\n {\n agentConnectionModuleName: getPythonAgentConnectionModuleName(tree),\n agentNameClassName,\n nameKebabCase,\n },\n { overwriteStrategy: OverwriteStrategy.KeepExisting },\n );\n addGuardrailPolicy(tree, agentDir);\n\n const agentFile = joinPathFragments(agentDir, 'agent.py');\n if (framework === 'langchain') {\n await addGuardrailedModelToLangChainAgent(tree, agentFile);\n } else {\n await addGuardrailedModelToStrandsAgent(tree, agentFile);\n }\n\n let wiringMessage: string | undefined;\n if (iac) {\n await addGuardrailInfra(tree, {\n iac,\n nameClassName: agentNameClassName,\n nameKebabCase,\n policyPath,\n });\n if (iac === 'cdk') {\n for (const message of addGuardrailPolicySynthInputs(tree, [policyPath])) {\n logger.warn(message);\n }\n }\n wiringMessage = guardrailWiringMessage({\n iac,\n nameClassName: agentNameClassName,\n nameKebabCase,\n npmScope: getNpmScope(tree),\n deployGuideUrl: PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL,\n });\n } else {\n logger.warn(\n `Agent ${agentTargetPrefix} in project ${project.name} has no infrastructure, so no guardrail infrastructure was generated. Create the guardrail yourself and write its ID and version to runtime config at agentcore.guardrails.${agentNameClassName} as { guardrailId, guardrailVersion }.`,\n );\n }\n\n addComponentGeneratorMetadata(\n tree,\n project.name,\n PY_AGENT_GUARDRAILS_GENERATOR_INFO,\n agentPath,\n agentTargetPrefix,\n );\n\n await addGeneratorMetricsIfApplicable(tree, [\n PY_AGENT_GUARDRAILS_GENERATOR_INFO,\n ]);\n\n await formatFilesInSubtree(tree);\n\n // Printed from the callback so it appears after Nx lists the changed\n // files, on every run, since the generator cannot tell whether the stack is\n // already wired.\n return () => {\n if (wiringMessage) {\n logger.info(wiringMessage);\n }\n };\n};\n\nexport default pyAgentGuardrailsGenerator;\n"],"names":["generateFiles","joinPathFragments","logger","OverwriteStrategy","addGuardrailPolicySynthInputs","getPythonAgentConnectionModuleName","addPythonDestructuredImport","applyGritQL","matchGritQL","formatFilesInSubtree","addGuardrailInfra","addGuardrailPolicy","guardrailWiringMessage","PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL","addGeneratorMetricsIfApplicable","kebabCase","toClassName","getNpmScope","addComponentGeneratorMetadata","getGeneratorInfo","readProjectConfigurationUnqualified","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","PY_AGENT_GENERATOR_INFO","PY_AGENT_GUARDRAILS_GENERATOR_INFO","filename","resolveAgent","projectName","components","agent","agents","filter","c","generator","id","available","map","name","join","match","find","Error","length","assertNoOtherAgentGuardrail","tree","iac","nameClassName","nameKebabCase","policyPath","file","exists","contents","read","some","ref","includes","addGuardrailedModelToStrandsAgent","agentFile","warn","addGuardrailedModelToLangChainAgent","pyAgentGuardrailsGenerator","options","project","metadata","agentTargetPrefix","agentNameClassName","rc","framework","moduleName","sourceRoot","root","split","pop","agentPath","path","agentDir","dirname","agentConnectionModuleName","overwriteStrategy","KeepExisting","wiringMessage","message","npmScope","deployGuideUrl","info"],"mappings":"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"}