@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.
- package/README.md +1 -0
- package/generators.json +8 -0
- package/migrations.json +6 -1
- package/package.json +1 -1
- package/src/internal/test-matrix/generator.js +6 -1
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/metadata.json +3 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
- package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
- package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
- package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
- package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
- package/src/py/agent/guardrails/generator.d.ts +10 -0
- package/src/py/agent/guardrails/generator.js +175 -0
- package/src/py/agent/guardrails/generator.js.map +1 -0
- package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
- package/src/py/agent/guardrails/schema.d.js +6 -0
- package/src/py/agent/guardrails/schema.d.js.map +1 -0
- package/src/py/agent/guardrails/schema.d.ts +9 -0
- package/src/py/agent/guardrails/schema.json +25 -0
- package/src/sdk/py.d.ts +2 -0
- package/src/sdk/py.js +1 -0
- package/src/sdk/py.js.map +1 -1
- package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +54 -3
- package/src/ts/agent/guardrails/generator.js +2 -1
- package/src/ts/agent/guardrails/generator.js.map +1 -1
- package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
- package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
- package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
- package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
- package/src/utils/test/fake-aws/fake_aws.py +205 -0
- package/src/utils/test/python-dependencies.d.ts +6 -0
- package/src/utils/test/python-dependencies.js +19 -0
- package/src/utils/test/python-dependencies.js.map +1 -1
- package/src/utils/test/warm-python-cache.js +5 -1
- package/src/utils/test/warm-python-cache.js.map +1 -1
- /package/src/{ts/agent/guardrails/files/agent → utils/guardrail-constructs/files/policy}/guardrail.json.template +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/ import { snakeCase } from "../names.js";
|
|
5
5
|
import { SHARED_CONSTRUCTS_NAME } from "../shared-constructs-constants.js";
|
|
6
6
|
/** The ts#agent guide section that shows how to deploy the agent's Terraform module. */ export const TS_AGENT_TERRAFORM_DEPLOY_GUIDE_URL = 'https://awslabs.github.io/nx-plugin-for-aws/en/guides/ts-agent/#infrastructure-as-code';
|
|
7
|
+
/** The py#agent guide section that shows how to deploy the agent's Terraform module. */ export const PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL = 'https://awslabs.github.io/nx-plugin-for-aws/en/guides/py-agent/#infrastructure-as-code';
|
|
7
8
|
/**
|
|
8
9
|
* The code a user adds to their CDK stack to guard the agent.
|
|
9
10
|
* The guide shows the same code for `my-agent` in scope `my-scope`, which
|
|
@@ -39,11 +40,11 @@ module "runtime_config_appconfig_deployment" {
|
|
|
39
40
|
}`;
|
|
40
41
|
};
|
|
41
42
|
/**
|
|
42
|
-
* How to add the printed Terraform code.
|
|
43
|
-
* the root module, so its `module` block may not exist yet.
|
|
44
|
-
*/ const terraformSteps = (nameKebabCase)=>{
|
|
43
|
+
* How to add the printed Terraform code. The agent generators do not add the
|
|
44
|
+
* agent to the root module, so its `module` block may not exist yet.
|
|
45
|
+
*/ const terraformSteps = (nameKebabCase, deployGuideUrl = TS_AGENT_TERRAFORM_DEPLOY_GUIDE_URL)=>{
|
|
45
46
|
const agentModule = snakeCase(nameKebabCase);
|
|
46
|
-
return `Add the module "${agentModule}_guardrail" block as printed below. Add its additional_iam_policy_statements line to your existing module "${agentModule}" block, and add module.${agentModule}_guardrail to the depends_on list of your runtime_config_appconfig_deployment module. If your root module does not deploy the agent yet, add the agent's module first, as described in ${
|
|
47
|
+
return `Add the module "${agentModule}_guardrail" block as printed below. Add its additional_iam_policy_statements line to your existing module "${agentModule}" block, and add module.${agentModule}_guardrail to the depends_on list of your runtime_config_appconfig_deployment module. If your root module does not deploy the agent yet, add the agent's module first, as described in ${deployGuideUrl}.`;
|
|
47
48
|
};
|
|
48
49
|
/**
|
|
49
50
|
* The message the generator prints after it adds the guardrail
|
|
@@ -53,7 +54,7 @@ module "runtime_config_appconfig_deployment" {
|
|
|
53
54
|
const missing = `Until you add it, the deployed agent fails on its first request, because runtime config has no guardrail at agentcore.guardrails.${options.nameClassName}.`;
|
|
54
55
|
return options.iac === 'cdk' ? `Add the guardrail to your agent in your stack, for example packages/infra/src/stacks/application-stack.ts. ${missing}
|
|
55
56
|
|
|
56
|
-
${cdkGuardrailWiring(options)}` : `Add the guardrail to your agent in the root Terraform module where you deploy the agent, for example packages/infra/src/main.tf. ${missing} ${terraformSteps(options.nameKebabCase)}
|
|
57
|
+
${cdkGuardrailWiring(options)}` : `Add the guardrail to your agent in the root Terraform module where you deploy the agent, for example packages/infra/src/main.tf. ${missing} ${terraformSteps(options.nameKebabCase, options.deployGuideUrl)}
|
|
57
58
|
|
|
58
59
|
${terraformGuardrailWiring(options)}`;
|
|
59
60
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/guardrail-constructs/guardrail-wiring.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { Iac } from '../iac.js';\nimport { snakeCase } from '../names.js';\nimport { SHARED_CONSTRUCTS_NAME } from '../shared-constructs-constants.js';\n\nexport interface GuardrailWiringOptions {\n iac: Iac;\n /** Class name of the guarded agent, eg `MyAgent`. */\n nameClassName: string;\n /** Kebab-case name of the guarded agent, eg `my-agent`. */\n nameKebabCase: string;\n /** npm scope of the workspace without the `@`, eg `my-scope`. */\n npmScope: string;\n}\n\n/** The ts#agent guide section that shows how to deploy the agent's Terraform module. */\nexport const TS_AGENT_TERRAFORM_DEPLOY_GUIDE_URL =\n 'https://awslabs.github.io/nx-plugin-for-aws/en/guides/ts-agent/#infrastructure-as-code';\n\n/**\n * The code a user adds to their CDK stack to guard the agent.\n * The guide shows the same code for `my-agent` in scope `my-scope`, which\n * generator.spec.ts checks.\n */\nexport const cdkGuardrailWiring = ({\n nameClassName,\n npmScope,\n}: Pick<GuardrailWiringOptions, 'nameClassName' | 'npmScope'>): string =>\n `import { ${nameClassName}, ${nameClassName}Guardrail } from '@${npmScope}/${SHARED_CONSTRUCTS_NAME}';\n\nconst agent = new ${nameClassName}(this, '${nameClassName}');\nnew ${nameClassName}Guardrail(this, '${nameClassName}Guardrail', { agent });`;\n\n/**\n * The code a user adds to their root Terraform module to grant the agent the\n * guardrail and deploy its runtime-config entry, relative to\n * `packages/infra/src`. The guide shows the same code for `my-agent`, which\n * generator.spec.ts checks.\n */\nexport const terraformGuardrailWiring = ({\n nameKebabCase,\n}: Pick<GuardrailWiringOptions, 'nameKebabCase'>): string => {\n const agentModule = snakeCase(nameKebabCase);\n const guardrailModule = `${agentModule}_guardrail`;\n return `module \"${guardrailModule}\" {\n source = \"../../common/terraform/src/app/guardrails/${nameKebabCase}\"\n}\n\nmodule \"${agentModule}\" {\n source = \"../../common/terraform/src/app/agents/${nameKebabCase}\"\n # ...existing arguments...\n additional_iam_policy_statements = module.${guardrailModule}.iam_policy_statements\n}\n\nmodule \"runtime_config_appconfig_deployment\" {\n # ...existing arguments...\n depends_on = [\n # ...existing modules...\n module.${guardrailModule},\n ]\n}`;\n};\n\n/**\n * How to add the printed Terraform code.
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/guardrail-constructs/guardrail-wiring.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { Iac } from '../iac.js';\nimport { snakeCase } from '../names.js';\nimport { SHARED_CONSTRUCTS_NAME } from '../shared-constructs-constants.js';\n\nexport interface GuardrailWiringOptions {\n iac: Iac;\n /** Class name of the guarded agent, eg `MyAgent`. */\n nameClassName: string;\n /** Kebab-case name of the guarded agent, eg `my-agent`. */\n nameKebabCase: string;\n /** npm scope of the workspace without the `@`, eg `my-scope`. */\n npmScope: string;\n /**\n * The agent guide section that shows how to deploy the agent's Terraform\n * module. Defaults to the ts#agent guide.\n */\n deployGuideUrl?: string;\n}\n\n/** The ts#agent guide section that shows how to deploy the agent's Terraform module. */\nexport const TS_AGENT_TERRAFORM_DEPLOY_GUIDE_URL =\n 'https://awslabs.github.io/nx-plugin-for-aws/en/guides/ts-agent/#infrastructure-as-code';\n\n/** The py#agent guide section that shows how to deploy the agent's Terraform module. */\nexport const PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL =\n 'https://awslabs.github.io/nx-plugin-for-aws/en/guides/py-agent/#infrastructure-as-code';\n\n/**\n * The code a user adds to their CDK stack to guard the agent.\n * The guide shows the same code for `my-agent` in scope `my-scope`, which\n * generator.spec.ts checks.\n */\nexport const cdkGuardrailWiring = ({\n nameClassName,\n npmScope,\n}: Pick<GuardrailWiringOptions, 'nameClassName' | 'npmScope'>): string =>\n `import { ${nameClassName}, ${nameClassName}Guardrail } from '@${npmScope}/${SHARED_CONSTRUCTS_NAME}';\n\nconst agent = new ${nameClassName}(this, '${nameClassName}');\nnew ${nameClassName}Guardrail(this, '${nameClassName}Guardrail', { agent });`;\n\n/**\n * The code a user adds to their root Terraform module to grant the agent the\n * guardrail and deploy its runtime-config entry, relative to\n * `packages/infra/src`. The guide shows the same code for `my-agent`, which\n * generator.spec.ts checks.\n */\nexport const terraformGuardrailWiring = ({\n nameKebabCase,\n}: Pick<GuardrailWiringOptions, 'nameKebabCase'>): string => {\n const agentModule = snakeCase(nameKebabCase);\n const guardrailModule = `${agentModule}_guardrail`;\n return `module \"${guardrailModule}\" {\n source = \"../../common/terraform/src/app/guardrails/${nameKebabCase}\"\n}\n\nmodule \"${agentModule}\" {\n source = \"../../common/terraform/src/app/agents/${nameKebabCase}\"\n # ...existing arguments...\n additional_iam_policy_statements = module.${guardrailModule}.iam_policy_statements\n}\n\nmodule \"runtime_config_appconfig_deployment\" {\n # ...existing arguments...\n depends_on = [\n # ...existing modules...\n module.${guardrailModule},\n ]\n}`;\n};\n\n/**\n * How to add the printed Terraform code. The agent generators do not add the\n * agent to the root module, so its `module` block may not exist yet.\n */\nconst terraformSteps = (\n nameKebabCase: string,\n deployGuideUrl = TS_AGENT_TERRAFORM_DEPLOY_GUIDE_URL,\n): string => {\n const agentModule = snakeCase(nameKebabCase);\n return `Add the module \"${agentModule}_guardrail\" block as printed below. Add its additional_iam_policy_statements line to your existing module \"${agentModule}\" block, and add module.${agentModule}_guardrail to the depends_on list of your runtime_config_appconfig_deployment module. If your root module does not deploy the agent yet, add the agent's module first, as described in ${deployGuideUrl}.`;\n};\n\n/**\n * The message the generator prints after it adds the guardrail\n * infrastructure. The generator does not edit the user's stack or root\n * module, so this tells the user what to add there.\n */\nexport const guardrailWiringMessage = (\n options: GuardrailWiringOptions,\n): string => {\n const missing = `Until you add it, the deployed agent fails on its first request, because runtime config has no guardrail at agentcore.guardrails.${options.nameClassName}.`;\n return options.iac === 'cdk'\n ? `Add the guardrail to your agent in your stack, for example packages/infra/src/stacks/application-stack.ts. ${missing}\n\n${cdkGuardrailWiring(options)}`\n : `Add the guardrail to your agent in the root Terraform module where you deploy the agent, for example packages/infra/src/main.tf. ${missing} ${terraformSteps(options.nameKebabCase, options.deployGuideUrl)}\n\n${terraformGuardrailWiring(options)}`;\n};\n"],"names":["snakeCase","SHARED_CONSTRUCTS_NAME","TS_AGENT_TERRAFORM_DEPLOY_GUIDE_URL","PY_AGENT_TERRAFORM_DEPLOY_GUIDE_URL","cdkGuardrailWiring","nameClassName","npmScope","terraformGuardrailWiring","nameKebabCase","agentModule","guardrailModule","terraformSteps","deployGuideUrl","guardrailWiringMessage","options","missing","iac"],"mappings":"AAAA;;;CAGC,GAED,SAASA,SAAS,QAAQ,cAAc;AACxC,SAASC,sBAAsB,QAAQ,oCAAoC;AAiB3E,sFAAsF,GACtF,OAAO,MAAMC,sCACX,yFAAyF;AAE3F,sFAAsF,GACtF,OAAO,MAAMC,sCACX,yFAAyF;AAE3F;;;;CAIC,GACD,OAAO,MAAMC,qBAAqB,CAAC,EACjCC,aAAa,EACbC,QAAQ,EACmD,GAC3D,CAAC,SAAS,EAAED,cAAc,EAAE,EAAEA,cAAc,mBAAmB,EAAEC,SAAS,CAAC,EAAEL,uBAAuB;;kBAEpF,EAAEI,cAAc,QAAQ,EAAEA,cAAc;IACtD,EAAEA,cAAc,iBAAiB,EAAEA,cAAc,uBAAuB,CAAC,CAAC;AAE9E;;;;;CAKC,GACD,OAAO,MAAME,2BAA2B,CAAC,EACvCC,aAAa,EACiC;IAC9C,MAAMC,cAAcT,UAAUQ;IAC9B,MAAME,kBAAkB,GAAGD,YAAY,UAAU,CAAC;IAClD,OAAO,CAAC,QAAQ,EAAEC,gBAAgB;sDACkB,EAAEF,cAAc;;;QAG9D,EAAEC,YAAY;kDAC4B,EAAED,cAAc;;4CAEtB,EAAEE,gBAAgB;;;;;;;WAOnD,EAAEA,gBAAgB;;CAE5B,CAAC;AACF,EAAE;AAEF;;;CAGC,GACD,MAAMC,iBAAiB,CACrBH,eACAI,iBAAiBV,mCAAmC;IAEpD,MAAMO,cAAcT,UAAUQ;IAC9B,OAAO,CAAC,gBAAgB,EAAEC,YAAY,2GAA2G,EAAEA,YAAY,wBAAwB,EAAEA,YAAY,uLAAuL,EAAEG,eAAe,CAAC,CAAC;AACjZ;AAEA;;;;CAIC,GACD,OAAO,MAAMC,yBAAyB,CACpCC;IAEA,MAAMC,UAAU,CAAC,iIAAiI,EAAED,QAAQT,aAAa,CAAC,CAAC,CAAC;IAC5K,OAAOS,QAAQE,GAAG,KAAK,QACnB,CAAC,2GAA2G,EAAED,QAAQ;;AAE5H,EAAEX,mBAAmBU,UAAU,GACzB,CAAC,iIAAiI,EAAEC,QAAQ,CAAC,EAAEJ,eAAeG,QAAQN,aAAa,EAAEM,QAAQF,cAAc,EAAE;;AAEnN,EAAEL,yBAAyBO,UAAU;AACrC,EAAE"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
"""Local stand-in for AppConfigData and Bedrock Runtime, so tests of generated
|
|
6
|
+
agent code send real SDK requests without AWS. Standard library only.
|
|
7
|
+
|
|
8
|
+
Usage: python fake_aws.py <config.json> <port> <requests.log>
|
|
9
|
+
or, in-process: start_server(config_file, log_file) returns (server, port).
|
|
10
|
+
|
|
11
|
+
- AppConfigData: POST /configurationsessions and GET /configuration serve the
|
|
12
|
+
JSON in <config.json>, re-read on every request, so editing the file changes
|
|
13
|
+
the value. A session whose ApplicationIdentifier is "broken" answers HTTP 400
|
|
14
|
+
BadRequestException, to simulate a read failure.
|
|
15
|
+
- Bedrock Runtime: POST /model/<id>/converse-stream (event stream) and
|
|
16
|
+
/model/<id>/converse (JSON) answer "4". A request with a guardrailConfig gets
|
|
17
|
+
BLOCKED_ANSWER with stop reason guardrail_intervened and a BLOCKED trace when
|
|
18
|
+
the text the guardrail checks contains DENIED_TERM. As in Bedrock, the
|
|
19
|
+
guardrail checks only the guardContent blocks if the messages hold any, and
|
|
20
|
+
every message otherwise.
|
|
21
|
+
|
|
22
|
+
Every request appends one JSON line to <requests.log>: AppConfigData requests
|
|
23
|
+
as {"path": "StartConfigurationSession" | "GetLatestConfiguration"}, model
|
|
24
|
+
requests as {"path", "modelId", "guardrailConfig", "userTexts", "intervened"}.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import binascii
|
|
28
|
+
import json
|
|
29
|
+
import struct
|
|
30
|
+
import sys
|
|
31
|
+
import threading
|
|
32
|
+
import urllib.parse
|
|
33
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
34
|
+
|
|
35
|
+
DENIED_TERM = "stocks"
|
|
36
|
+
BLOCKED_ANSWER = "Sorry, I can't help with that request."
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _header(name: str, value: str) -> bytes:
|
|
40
|
+
n, v = name.encode(), value.encode()
|
|
41
|
+
return struct.pack("B", len(n)) + n + b"\x07" + struct.pack(">H", len(v)) + v
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _event(event_type: str, payload: dict) -> bytes:
|
|
45
|
+
headers = (
|
|
46
|
+
_header(":event-type", event_type)
|
|
47
|
+
+ _header(":content-type", "application/json")
|
|
48
|
+
+ _header(":message-type", "event")
|
|
49
|
+
)
|
|
50
|
+
body = json.dumps(payload).encode()
|
|
51
|
+
total = 12 + len(headers) + len(body) + 4
|
|
52
|
+
prelude = struct.pack(">II", total, len(headers))
|
|
53
|
+
prelude += struct.pack(">I", binascii.crc32(prelude) & 0xFFFFFFFF)
|
|
54
|
+
message = prelude + headers + body
|
|
55
|
+
return message + struct.pack(">I", binascii.crc32(message) & 0xFFFFFFFF)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _texts(blocks: list) -> list[str]:
|
|
59
|
+
texts = []
|
|
60
|
+
for block in blocks:
|
|
61
|
+
if "text" in block:
|
|
62
|
+
texts.append(block["text"])
|
|
63
|
+
elif "guardContent" in block:
|
|
64
|
+
texts.append(block["guardContent"].get("text", {}).get("text", ""))
|
|
65
|
+
elif "toolResult" in block:
|
|
66
|
+
texts.extend(_texts(block["toolResult"].get("content", [])))
|
|
67
|
+
return texts
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _intervenes(request: dict) -> bool:
|
|
71
|
+
"""Whether the guardrail blocks the request's input, by Bedrock's rule for guardContent."""
|
|
72
|
+
if not request.get("guardrailConfig"):
|
|
73
|
+
return False
|
|
74
|
+
messages = request.get("messages", [])
|
|
75
|
+
guarded = [
|
|
76
|
+
block["guardContent"].get("text", {}).get("text", "")
|
|
77
|
+
for message in messages
|
|
78
|
+
for block in message.get("content", [])
|
|
79
|
+
if "guardContent" in block
|
|
80
|
+
]
|
|
81
|
+
checked = guarded or [text for message in messages for text in _texts(message.get("content", []))]
|
|
82
|
+
return any(DENIED_TERM in text.lower() for text in checked)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_BLOCKED_TRACE = {
|
|
86
|
+
"guardrail": {
|
|
87
|
+
"inputAssessment": {
|
|
88
|
+
"grtest": {
|
|
89
|
+
"topicPolicy": {
|
|
90
|
+
"topics": [{"name": "Investment advice", "type": "DENY", "action": "BLOCKED", "detected": True}]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _handler(config_file: str, log_file: str) -> type[BaseHTTPRequestHandler]:
|
|
99
|
+
lock = threading.Lock()
|
|
100
|
+
|
|
101
|
+
def log(entry: dict) -> None:
|
|
102
|
+
with lock, open(log_file, "a") as f:
|
|
103
|
+
f.write(json.dumps(entry) + "\n")
|
|
104
|
+
|
|
105
|
+
class Handler(BaseHTTPRequestHandler):
|
|
106
|
+
protocol_version = "HTTP/1.1"
|
|
107
|
+
|
|
108
|
+
def log_message(self, *_args) -> None:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def _send(self, status: int, body: bytes, content_type: str, extra=None) -> None:
|
|
112
|
+
self.send_response(status)
|
|
113
|
+
self.send_header("Content-Type", content_type)
|
|
114
|
+
self.send_header("Content-Length", str(len(body)))
|
|
115
|
+
for k, v in (extra or {}).items():
|
|
116
|
+
self.send_header(k, v)
|
|
117
|
+
self.end_headers()
|
|
118
|
+
self.wfile.write(body)
|
|
119
|
+
|
|
120
|
+
def do_GET(self) -> None:
|
|
121
|
+
if self.path.startswith("/configuration"):
|
|
122
|
+
log({"path": "GetLatestConfiguration"})
|
|
123
|
+
with open(config_file, "rb") as f:
|
|
124
|
+
body = f.read()
|
|
125
|
+
self._send(
|
|
126
|
+
200,
|
|
127
|
+
body,
|
|
128
|
+
"application/json",
|
|
129
|
+
{"Next-Poll-Configuration-Token": "token", "Next-Poll-Interval-In-Seconds": "60"},
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
self._send(404, b"{}", "application/json")
|
|
133
|
+
|
|
134
|
+
def do_POST(self) -> None:
|
|
135
|
+
raw = self.rfile.read(int(self.headers.get("Content-Length", "0")))
|
|
136
|
+
if self.path.startswith("/configurationsessions"):
|
|
137
|
+
log({"path": "StartConfigurationSession"})
|
|
138
|
+
if json.loads(raw or b"{}").get("ApplicationIdentifier") == "broken":
|
|
139
|
+
self._send(
|
|
140
|
+
400,
|
|
141
|
+
json.dumps({"Message": "Application broken does not exist."}).encode(),
|
|
142
|
+
"application/json",
|
|
143
|
+
{"x-amzn-ErrorType": "BadRequestException"},
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
self._send(201, json.dumps({"InitialConfigurationToken": "token"}).encode(), "application/json")
|
|
147
|
+
return
|
|
148
|
+
if self.path.startswith("/model/"):
|
|
149
|
+
request = json.loads(raw or b"{}")
|
|
150
|
+
model_id = urllib.parse.unquote(self.path.split("/")[2])
|
|
151
|
+
intervened = _intervenes(request)
|
|
152
|
+
log(
|
|
153
|
+
{
|
|
154
|
+
"path": self.path.split("/")[-1],
|
|
155
|
+
"modelId": model_id,
|
|
156
|
+
"guardrailConfig": request.get("guardrailConfig"),
|
|
157
|
+
"userTexts": [
|
|
158
|
+
text
|
|
159
|
+
for message in request.get("messages", [])
|
|
160
|
+
if message.get("role") == "user"
|
|
161
|
+
for text in _texts(message.get("content", []))
|
|
162
|
+
],
|
|
163
|
+
"intervened": intervened,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
text = BLOCKED_ANSWER if intervened else "4"
|
|
167
|
+
stop_reason = "guardrail_intervened" if intervened else "end_turn"
|
|
168
|
+
metadata = {
|
|
169
|
+
"usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 2},
|
|
170
|
+
"metrics": {"latencyMs": 1},
|
|
171
|
+
**({"trace": _BLOCKED_TRACE} if intervened else {}),
|
|
172
|
+
}
|
|
173
|
+
if self.path.endswith("/converse-stream"):
|
|
174
|
+
body = b"".join(
|
|
175
|
+
[
|
|
176
|
+
_event("messageStart", {"role": "assistant"}),
|
|
177
|
+
_event("contentBlockDelta", {"contentBlockIndex": 0, "delta": {"text": text}}),
|
|
178
|
+
_event("contentBlockStop", {"contentBlockIndex": 0}),
|
|
179
|
+
_event("messageStop", {"stopReason": stop_reason}),
|
|
180
|
+
_event("metadata", metadata),
|
|
181
|
+
]
|
|
182
|
+
)
|
|
183
|
+
self._send(200, body, "application/vnd.amazon.eventstream")
|
|
184
|
+
return
|
|
185
|
+
answer = {
|
|
186
|
+
"output": {"message": {"role": "assistant", "content": [{"text": text}]}},
|
|
187
|
+
"stopReason": stop_reason,
|
|
188
|
+
**metadata,
|
|
189
|
+
}
|
|
190
|
+
self._send(200, json.dumps(answer).encode(), "application/json")
|
|
191
|
+
return
|
|
192
|
+
self._send(404, b"{}", "application/json")
|
|
193
|
+
|
|
194
|
+
return Handler
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def start_server(config_file: str, log_file: str, port: int = 0) -> tuple[ThreadingHTTPServer, int]:
|
|
198
|
+
"""Serve on 127.0.0.1 in a daemon thread; port 0 picks a free port."""
|
|
199
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), _handler(config_file, log_file))
|
|
200
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
201
|
+
return server, server.server_address[1]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
ThreadingHTTPServer(("127.0.0.1", int(sys.argv[2])), _handler(sys.argv[1], sys.argv[3])).serve_forever()
|
|
@@ -12,3 +12,9 @@ export declare const PY_VERIFIER_TYPE_CHECKER: string;
|
|
|
12
12
|
* they are included too.
|
|
13
13
|
*/
|
|
14
14
|
export declare const PY_DYNAMODB_VERIFIER_DEPENDENCIES: string[];
|
|
15
|
+
/**
|
|
16
|
+
* The packages a Strands or LangChain agent generated by `py#agent` imports
|
|
17
|
+
* from agent.py and guardrail.py, at the versions the generator vends, plus
|
|
18
|
+
* the ruff the vended lint and format targets run.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES: string[];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/ import { OPEN_API_PY_CLIENT_DEPENDENCIES } from "../../open-api/py-client/generator.js";
|
|
5
|
+
import { DEPENDENCIES as PY_AGENT_DEPENDENCIES } from "../../py/agent/generator.js";
|
|
5
6
|
import { DEPENDENCIES as PY_DYNAMODB_DEPENDENCIES } from "../../py/dynamodb/generator.js";
|
|
6
7
|
import { declaredNames } from "../declared-dependencies.js";
|
|
7
8
|
import { PY_VERSIONS, withPyVersions } from "../versions.js";
|
|
@@ -19,5 +20,23 @@ import { PY_VERSIONS, withPyVersions } from "../versions.js";
|
|
|
19
20
|
...withPyVersions(PY_DYNAMODB_DEPENDENCIES, declaredNames(PY_DYNAMODB_DEPENDENCIES.py)),
|
|
20
21
|
...PY_CLIENT_VERIFIER_DEPENDENCIES
|
|
21
22
|
];
|
|
23
|
+
/**
|
|
24
|
+
* The packages a Strands or LangChain agent generated by `py#agent` imports
|
|
25
|
+
* from agent.py and guardrail.py, at the versions the generator vends, plus
|
|
26
|
+
* the ruff the vended lint and format targets run.
|
|
27
|
+
*/ export const PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES = [
|
|
28
|
+
...withPyVersions(PY_AGENT_DEPENDENCIES, [
|
|
29
|
+
'aws-lambda-powertools',
|
|
30
|
+
'boto3',
|
|
31
|
+
'strands-agents',
|
|
32
|
+
'strands-agents-tools',
|
|
33
|
+
'langchain',
|
|
34
|
+
'langchain-aws',
|
|
35
|
+
'langgraph',
|
|
36
|
+
'langgraph-checkpoint-sqlite',
|
|
37
|
+
'aiosqlite'
|
|
38
|
+
]),
|
|
39
|
+
`ruff${PY_VERSIONS.ruff}`
|
|
40
|
+
];
|
|
22
41
|
|
|
23
42
|
//# sourceMappingURL=python-dependencies.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/test/python-dependencies.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { OPEN_API_PY_CLIENT_DEPENDENCIES } from '../../open-api/py-client/generator.js';\nimport { DEPENDENCIES as PY_DYNAMODB_DEPENDENCIES } from '../../py/dynamodb/generator.js';\nimport { declaredNames } from '../declared-dependencies.js';\nimport { PY_VERSIONS, withPyVersions } from '../versions.js';\n\n/**\n * The packages a client generated by `open-api#py-client` imports, at the\n * versions the generator vends — so a worker runs the code under test against\n * exactly what a consumer gets.\n */\nexport const PY_CLIENT_VERIFIER_DEPENDENCIES: string[] = withPyVersions(\n OPEN_API_PY_CLIENT_DEPENDENCIES,\n declaredNames(OPEN_API_PY_CLIENT_DEPENDENCIES.py),\n);\n\n/** The type checker every Python worker installs alongside its dependencies. */\nexport const PY_VERIFIER_TYPE_CHECKER = `ty${PY_VERSIONS.ty}`;\n\n/**\n * The packages a project generated by `py#dynamodb` imports, at the versions\n * the generator vends. The worker itself imports the py-client packages, so\n * they are included too.\n */\nexport const PY_DYNAMODB_VERIFIER_DEPENDENCIES: string[] = [\n ...withPyVersions(\n PY_DYNAMODB_DEPENDENCIES,\n declaredNames(PY_DYNAMODB_DEPENDENCIES.py),\n ),\n ...PY_CLIENT_VERIFIER_DEPENDENCIES,\n];\n"],"names":["OPEN_API_PY_CLIENT_DEPENDENCIES","DEPENDENCIES","PY_DYNAMODB_DEPENDENCIES","declaredNames","PY_VERSIONS","withPyVersions","PY_CLIENT_VERIFIER_DEPENDENCIES","py","PY_VERIFIER_TYPE_CHECKER","ty","PY_DYNAMODB_VERIFIER_DEPENDENCIES"],"mappings":"AAAA;;;CAGC,GACD,SAASA,+BAA+B,QAAQ,wCAAwC;AACxF,SAASC,gBAAgBC,wBAAwB,QAAQ,iCAAiC;AAC1F,SAASC,aAAa,QAAQ,8BAA8B;AAC5D,SAASC,WAAW,EAAEC,cAAc,QAAQ,iBAAiB;AAE7D;;;;CAIC,GACD,OAAO,MAAMC,kCAA4CD,
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/test/python-dependencies.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { OPEN_API_PY_CLIENT_DEPENDENCIES } from '../../open-api/py-client/generator.js';\nimport { DEPENDENCIES as PY_AGENT_DEPENDENCIES } from '../../py/agent/generator.js';\nimport { DEPENDENCIES as PY_DYNAMODB_DEPENDENCIES } from '../../py/dynamodb/generator.js';\nimport { declaredNames } from '../declared-dependencies.js';\nimport { PY_VERSIONS, withPyVersions } from '../versions.js';\n\n/**\n * The packages a client generated by `open-api#py-client` imports, at the\n * versions the generator vends — so a worker runs the code under test against\n * exactly what a consumer gets.\n */\nexport const PY_CLIENT_VERIFIER_DEPENDENCIES: string[] = withPyVersions(\n OPEN_API_PY_CLIENT_DEPENDENCIES,\n declaredNames(OPEN_API_PY_CLIENT_DEPENDENCIES.py),\n);\n\n/** The type checker every Python worker installs alongside its dependencies. */\nexport const PY_VERIFIER_TYPE_CHECKER = `ty${PY_VERSIONS.ty}`;\n\n/**\n * The packages a project generated by `py#dynamodb` imports, at the versions\n * the generator vends. The worker itself imports the py-client packages, so\n * they are included too.\n */\nexport const PY_DYNAMODB_VERIFIER_DEPENDENCIES: string[] = [\n ...withPyVersions(\n PY_DYNAMODB_DEPENDENCIES,\n declaredNames(PY_DYNAMODB_DEPENDENCIES.py),\n ),\n ...PY_CLIENT_VERIFIER_DEPENDENCIES,\n];\n\n/**\n * The packages a Strands or LangChain agent generated by `py#agent` imports\n * from agent.py and guardrail.py, at the versions the generator vends, plus\n * the ruff the vended lint and format targets run.\n */\nexport const PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES: string[] = [\n ...withPyVersions(PY_AGENT_DEPENDENCIES, [\n 'aws-lambda-powertools',\n 'boto3',\n 'strands-agents',\n 'strands-agents-tools',\n 'langchain',\n 'langchain-aws',\n 'langgraph',\n 'langgraph-checkpoint-sqlite',\n 'aiosqlite',\n ]),\n `ruff${PY_VERSIONS.ruff}`,\n];\n"],"names":["OPEN_API_PY_CLIENT_DEPENDENCIES","DEPENDENCIES","PY_AGENT_DEPENDENCIES","PY_DYNAMODB_DEPENDENCIES","declaredNames","PY_VERSIONS","withPyVersions","PY_CLIENT_VERIFIER_DEPENDENCIES","py","PY_VERIFIER_TYPE_CHECKER","ty","PY_DYNAMODB_VERIFIER_DEPENDENCIES","PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES","ruff"],"mappings":"AAAA;;;CAGC,GACD,SAASA,+BAA+B,QAAQ,wCAAwC;AACxF,SAASC,gBAAgBC,qBAAqB,QAAQ,8BAA8B;AACpF,SAASD,gBAAgBE,wBAAwB,QAAQ,iCAAiC;AAC1F,SAASC,aAAa,QAAQ,8BAA8B;AAC5D,SAASC,WAAW,EAAEC,cAAc,QAAQ,iBAAiB;AAE7D;;;;CAIC,GACD,OAAO,MAAMC,kCAA4CD,eACvDN,iCACAI,cAAcJ,gCAAgCQ,EAAE,GAChD;AAEF,8EAA8E,GAC9E,OAAO,MAAMC,2BAA2B,CAAC,EAAE,EAAEJ,YAAYK,EAAE,EAAE,CAAC;AAE9D;;;;CAIC,GACD,OAAO,MAAMC,oCAA8C;OACtDL,eACDH,0BACAC,cAAcD,yBAAyBK,EAAE;OAExCD;CACJ,CAAC;AAEF;;;;CAIC,GACD,OAAO,MAAMK,2CAAqD;OAC7DN,eAAeJ,uBAAuB;QACvC;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD;IACD,CAAC,IAAI,EAAEG,YAAYQ,IAAI,EAAE;CAC1B,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/ import { execFileSync } from "child_process";
|
|
5
5
|
import { pyenvPythonVersion } from "../versions.js";
|
|
6
|
-
import { PY_CLIENT_VERIFIER_DEPENDENCIES, PY_DYNAMODB_VERIFIER_DEPENDENCIES, PY_VERIFIER_TYPE_CHECKER } from "./python-dependencies.js";
|
|
6
|
+
import { PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES, PY_CLIENT_VERIFIER_DEPENDENCIES, PY_DYNAMODB_VERIFIER_DEPENDENCIES, PY_VERIFIER_TYPE_CHECKER } from "./python-dependencies.js";
|
|
7
7
|
/**
|
|
8
8
|
* Vitest globalSetup that warms uv's cache once before any worker spawns.
|
|
9
9
|
*
|
|
@@ -31,6 +31,10 @@ import { PY_CLIENT_VERIFIER_DEPENDENCIES, PY_DYNAMODB_VERIFIER_DEPENDENCIES, PY_
|
|
|
31
31
|
{
|
|
32
32
|
dependencies: PY_DYNAMODB_VERIFIER_DEPENDENCIES,
|
|
33
33
|
python: pyenvPythonVersion()
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
dependencies: PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES,
|
|
37
|
+
python: pyenvPythonVersion()
|
|
34
38
|
}
|
|
35
39
|
];
|
|
36
40
|
for (const { dependencies, python } of environments){
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/test/warm-python-cache.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { execFileSync } from 'child_process';\nimport { pyenvPythonVersion } from '../versions.js';\nimport {\n PY_CLIENT_VERIFIER_DEPENDENCIES,\n PY_DYNAMODB_VERIFIER_DEPENDENCIES,\n PY_VERIFIER_TYPE_CHECKER,\n} from './python-dependencies.js';\n\n/**\n * Vitest globalSetup that warms uv's cache once before any worker spawns.\n *\n * `PythonVerifier` starts a `uv run --with …` worker per spec file. On a cold\n * cache the first invocation takes an exclusive write lock while it downloads\n * and installs those packages; with many workers racing that lock, startup\n * stalls and a worker can be torn down before it answers. Installing them once\n * up front means every worker hits a warm cache, where uv only takes shared\n * locks.\n *\n * Mirrors the pre-warm this suite used for `ruff` before it moved in-process.\n *\n * A failure here is not fatal — a missing `uv` is reported by the verifier\n * itself, with a message naming the tool — but it is warned about rather than\n * swallowed: a broken cache otherwise degrades silently to every worker racing a\n * cold one, which is the startup failure this exists to prevent.\n */\nexport default function setup() {\n // The same sets the verifiers install, on the same interpreters, so the warm\n // cache is the one they hit. Wheels can be interpreter-specific, so each set\n // is warmed on the interpreter its verifier runs.\n const environments: { dependencies: string[]; python?: string }[] = [\n { dependencies: PY_CLIENT_VERIFIER_DEPENDENCIES },\n {\n dependencies: PY_DYNAMODB_VERIFIER_DEPENDENCIES,\n python: pyenvPythonVersion(),\n },\n ];\n for (const { dependencies, python } of environments) {\n const deps = [...dependencies, PY_VERIFIER_TYPE_CHECKER].flatMap((spec) => [\n '--with',\n spec,\n ]);\n const interpreter = python ? ['--python', python] : [];\n try {\n execFileSync('uv', ['run', ...interpreter, ...deps, 'python', '-c', ''], {\n stdio: 'ignore',\n });\n } catch (error) {\n console.warn(\n `Could not warm uv's cache, so each Python worker will install its own ` +\n `dependencies and may time out starting up: ${error}`,\n );\n }\n }\n}\n"],"names":["execFileSync","pyenvPythonVersion","PY_CLIENT_VERIFIER_DEPENDENCIES","PY_DYNAMODB_VERIFIER_DEPENDENCIES","PY_VERIFIER_TYPE_CHECKER","setup","environments","dependencies","python","deps","flatMap","spec","interpreter","stdio","error","console","warn"],"mappings":"AAAA;;;CAGC,GACD,SAASA,YAAY,QAAQ,gBAAgB;AAC7C,SAASC,kBAAkB,QAAQ,iBAAiB;AACpD,SACEC,+BAA+B,EAC/BC,iCAAiC,EACjCC,wBAAwB,QACnB,2BAA2B;AAElC;;;;;;;;;;;;;;;;CAgBC,GACD,eAAe,SAASC;IACtB,6EAA6E;IAC7E,6EAA6E;IAC7E,kDAAkD;IAClD,MAAMC,eAA8D;QAClE;YAAEC,cAAcL;QAAgC;QAChD;YACEK,cAAcJ;YACdK,
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/test/warm-python-cache.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { execFileSync } from 'child_process';\nimport { pyenvPythonVersion } from '../versions.js';\nimport {\n PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES,\n PY_CLIENT_VERIFIER_DEPENDENCIES,\n PY_DYNAMODB_VERIFIER_DEPENDENCIES,\n PY_VERIFIER_TYPE_CHECKER,\n} from './python-dependencies.js';\n\n/**\n * Vitest globalSetup that warms uv's cache once before any worker spawns.\n *\n * `PythonVerifier` starts a `uv run --with …` worker per spec file. On a cold\n * cache the first invocation takes an exclusive write lock while it downloads\n * and installs those packages; with many workers racing that lock, startup\n * stalls and a worker can be torn down before it answers. Installing them once\n * up front means every worker hits a warm cache, where uv only takes shared\n * locks.\n *\n * Mirrors the pre-warm this suite used for `ruff` before it moved in-process.\n *\n * A failure here is not fatal — a missing `uv` is reported by the verifier\n * itself, with a message naming the tool — but it is warned about rather than\n * swallowed: a broken cache otherwise degrades silently to every worker racing a\n * cold one, which is the startup failure this exists to prevent.\n */\nexport default function setup() {\n // The same sets the verifiers install, on the same interpreters, so the warm\n // cache is the one they hit. Wheels can be interpreter-specific, so each set\n // is warmed on the interpreter its verifier runs.\n const environments: { dependencies: string[]; python?: string }[] = [\n { dependencies: PY_CLIENT_VERIFIER_DEPENDENCIES },\n {\n dependencies: PY_DYNAMODB_VERIFIER_DEPENDENCIES,\n python: pyenvPythonVersion(),\n },\n {\n dependencies: PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES,\n python: pyenvPythonVersion(),\n },\n ];\n for (const { dependencies, python } of environments) {\n const deps = [...dependencies, PY_VERIFIER_TYPE_CHECKER].flatMap((spec) => [\n '--with',\n spec,\n ]);\n const interpreter = python ? ['--python', python] : [];\n try {\n execFileSync('uv', ['run', ...interpreter, ...deps, 'python', '-c', ''], {\n stdio: 'ignore',\n });\n } catch (error) {\n console.warn(\n `Could not warm uv's cache, so each Python worker will install its own ` +\n `dependencies and may time out starting up: ${error}`,\n );\n }\n }\n}\n"],"names":["execFileSync","pyenvPythonVersion","PY_AGENT_GUARDRAIL_VERIFIER_DEPENDENCIES","PY_CLIENT_VERIFIER_DEPENDENCIES","PY_DYNAMODB_VERIFIER_DEPENDENCIES","PY_VERIFIER_TYPE_CHECKER","setup","environments","dependencies","python","deps","flatMap","spec","interpreter","stdio","error","console","warn"],"mappings":"AAAA;;;CAGC,GACD,SAASA,YAAY,QAAQ,gBAAgB;AAC7C,SAASC,kBAAkB,QAAQ,iBAAiB;AACpD,SACEC,wCAAwC,EACxCC,+BAA+B,EAC/BC,iCAAiC,EACjCC,wBAAwB,QACnB,2BAA2B;AAElC;;;;;;;;;;;;;;;;CAgBC,GACD,eAAe,SAASC;IACtB,6EAA6E;IAC7E,6EAA6E;IAC7E,kDAAkD;IAClD,MAAMC,eAA8D;QAClE;YAAEC,cAAcL;QAAgC;QAChD;YACEK,cAAcJ;YACdK,QAAQR;QACV;QACA;YACEO,cAAcN;YACdO,QAAQR;QACV;KACD;IACD,KAAK,MAAM,EAAEO,YAAY,EAAEC,MAAM,EAAE,IAAIF,aAAc;QACnD,MAAMG,OAAO;eAAIF;YAAcH;SAAyB,CAACM,OAAO,CAAC,CAACC,OAAS;gBACzE;gBACAA;aACD;QACD,MAAMC,cAAcJ,SAAS;YAAC;YAAYA;SAAO,GAAG,EAAE;QACtD,IAAI;YACFT,aAAa,MAAM;gBAAC;mBAAUa;mBAAgBH;gBAAM;gBAAU;gBAAM;aAAG,EAAE;gBACvEI,OAAO;YACT;QACF,EAAE,OAAOC,OAAO;YACdC,QAAQC,IAAI,CACV,CAAC,sEAAsE,CAAC,GACtE,CAAC,2CAA2C,EAAEF,OAAO;QAE3D;IACF;AACF"}
|
|
File without changes
|