bedrock-agentcore-starter-toolkit 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
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.
Potentially problematic release.
This version of bedrock-agentcore-starter-toolkit might be problematic. Click here for more details.
- bedrock_agentcore_starter_toolkit/__init__.py +5 -0
- bedrock_agentcore_starter_toolkit/cli/cli.py +32 -0
- bedrock_agentcore_starter_toolkit/cli/common.py +44 -0
- bedrock_agentcore_starter_toolkit/cli/gateway/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/cli/gateway/commands.py +88 -0
- bedrock_agentcore_starter_toolkit/cli/runtime/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +651 -0
- bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +133 -0
- bedrock_agentcore_starter_toolkit/notebook/__init__.py +5 -0
- bedrock_agentcore_starter_toolkit/notebook/runtime/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +239 -0
- bedrock_agentcore_starter_toolkit/operations/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/README.md +277 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/__init__.py +6 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/client.py +456 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/constants.py +152 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/create_lambda.py +85 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/create_role.py +90 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/exceptions.py +13 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +26 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +241 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +404 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +129 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +439 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/models.py +79 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/status.py +66 -0
- bedrock_agentcore_starter_toolkit/services/codebuild.py +332 -0
- bedrock_agentcore_starter_toolkit/services/ecr.py +84 -0
- bedrock_agentcore_starter_toolkit/services/runtime.py +473 -0
- bedrock_agentcore_starter_toolkit/utils/endpoints.py +32 -0
- bedrock_agentcore_starter_toolkit/utils/logging_config.py +72 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/config.py +129 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/container.py +310 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/entrypoint.py +197 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/logs.py +33 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/policy_template.py +74 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +151 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +44 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/dockerignore.template +68 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_policy.json.j2 +98 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_trust_policy.json.j2 +21 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/METADATA +137 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/RECORD +47 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/entry_points.txt +2 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/licenses/NOTICE.txt +190 -0
- bedrock_agentcore_starter_toolkit/init.py +0 -3
- bedrock_agentcore_starter_toolkit-0.0.1.dist-info/METADATA +0 -26
- bedrock_agentcore_starter_toolkit-0.0.1.dist-info/RECORD +0 -5
- {bedrock_agentcore_starter_toolkit-0.0.1.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/WHEEL +0 -0
- /bedrock_agentcore_starter_toolkit-0.0.1.dist-info/licenses/LICENSE → /bedrock_agentcore_starter_toolkit-0.1.1.dist-info/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Configuration utilities for Bedrock AgentCore SDK."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# def _clean_authorizer_config(config_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
14
|
+
# """Remove unwanted snake_case authorizer configurations."""
|
|
15
|
+
# if "authorizer_configuration" in config_dict:
|
|
16
|
+
# auth_config = config_dict["authorizer_configuration"]
|
|
17
|
+
# # Remove snake_case version if it exists
|
|
18
|
+
# if "custom_jwt_authorizer" in auth_config:
|
|
19
|
+
# del auth_config["custom_jwt_authorizer"]
|
|
20
|
+
# # If no valid camelCase configuration exists and auth_config is empty, remove it
|
|
21
|
+
# if not auth_config:
|
|
22
|
+
# del config_dict["authorizer_configuration"]
|
|
23
|
+
# return config_dict
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_project_config_format(config_path: Path) -> bool:
|
|
27
|
+
"""Check if config file uses project format (has 'agents' key)."""
|
|
28
|
+
if not config_path.exists():
|
|
29
|
+
return False
|
|
30
|
+
with open(config_path, "r") as f:
|
|
31
|
+
data = yaml.safe_load(f) or {}
|
|
32
|
+
return isinstance(data, dict) and "agents" in data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_legacy_format(data: dict) -> bool:
|
|
36
|
+
"""Detect old single-agent format."""
|
|
37
|
+
return isinstance(data, dict) and "agents" not in data and "name" in data and "entrypoint" in data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _transform_legacy_to_multi_agent(data: dict) -> BedrockAgentCoreConfigSchema:
|
|
41
|
+
"""Transform old format to new format at runtime."""
|
|
42
|
+
agent_config = BedrockAgentCoreAgentSchema.model_validate(data)
|
|
43
|
+
return BedrockAgentCoreConfigSchema(default_agent=agent_config.name, agents={agent_config.name: agent_config})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_config(config_path: Path) -> BedrockAgentCoreConfigSchema:
|
|
47
|
+
"""Load config with automatic legacy format transformation."""
|
|
48
|
+
if not config_path.exists():
|
|
49
|
+
raise FileNotFoundError(f"Configuration not found: {config_path}")
|
|
50
|
+
|
|
51
|
+
with open(config_path, "r") as f:
|
|
52
|
+
data = yaml.safe_load(f) or {}
|
|
53
|
+
|
|
54
|
+
# Auto-detect and transform legacy format
|
|
55
|
+
if _is_legacy_format(data):
|
|
56
|
+
return _transform_legacy_to_multi_agent(data)
|
|
57
|
+
|
|
58
|
+
# New format
|
|
59
|
+
try:
|
|
60
|
+
return BedrockAgentCoreConfigSchema.model_validate(data)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise ValueError(f"Invalid configuration format: {e}") from e
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def save_config(config: BedrockAgentCoreConfigSchema, config_path: Path):
|
|
66
|
+
"""Save configuration to YAML file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
config: BedrockAgentCoreConfigSchema instance to save
|
|
70
|
+
config_path: Path to save configuration file
|
|
71
|
+
"""
|
|
72
|
+
with open(config_path, "w") as f:
|
|
73
|
+
yaml.dump(config.model_dump(), f, default_flow_style=False, sort_keys=False)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_config_if_exists(config_path: Path) -> Optional[BedrockAgentCoreConfigSchema]:
|
|
77
|
+
"""Load configuration if file exists, otherwise return None.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config_path: Path to configuration file
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
BedrockAgentCoreConfigSchema instance or None if file doesn't exist
|
|
84
|
+
"""
|
|
85
|
+
if not config_path.exists():
|
|
86
|
+
return None
|
|
87
|
+
return load_config(config_path)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def merge_agent_config(
|
|
91
|
+
config_path: Path, agent_name: str, new_config: BedrockAgentCoreAgentSchema
|
|
92
|
+
) -> BedrockAgentCoreConfigSchema:
|
|
93
|
+
"""Merge agent configuration into config.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
config_path: Path to configuration file
|
|
97
|
+
agent_name: Name of the agent to add/update
|
|
98
|
+
new_config: Agent configuration to merge
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Updated project configuration
|
|
102
|
+
"""
|
|
103
|
+
config = load_config_if_exists(config_path)
|
|
104
|
+
|
|
105
|
+
# Handle None case - create new config
|
|
106
|
+
if config is None:
|
|
107
|
+
config = BedrockAgentCoreConfigSchema()
|
|
108
|
+
|
|
109
|
+
# Preserve deployment info if agent exists
|
|
110
|
+
if agent_name in config.agents:
|
|
111
|
+
new_config.bedrock_agentcore = config.agents[agent_name].bedrock_agentcore
|
|
112
|
+
|
|
113
|
+
# Add/update agent
|
|
114
|
+
config.agents[agent_name] = new_config
|
|
115
|
+
|
|
116
|
+
# Log default agent change and always set current agent as default
|
|
117
|
+
old_default = config.default_agent
|
|
118
|
+
if old_default != agent_name:
|
|
119
|
+
if old_default:
|
|
120
|
+
log.info("Changing default agent from '%s' to '%s'", old_default, agent_name)
|
|
121
|
+
else:
|
|
122
|
+
log.info("Setting '%s' as default agent", agent_name)
|
|
123
|
+
else:
|
|
124
|
+
log.info("Keeping '%s' as default agent", agent_name)
|
|
125
|
+
|
|
126
|
+
# Always set current agent as default (the agent being configured becomes the new default)
|
|
127
|
+
config.default_agent = agent_name
|
|
128
|
+
|
|
129
|
+
return config
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Container runtime management for Bedrock AgentCore SDK."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess # nosec B404 - Required for container runtime operations
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from jinja2 import Template
|
|
11
|
+
|
|
12
|
+
from ...cli.common import _handle_warn
|
|
13
|
+
from .entrypoint import detect_dependencies, get_python_version
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ContainerRuntime:
|
|
19
|
+
"""Container runtime for Docker, Finch, and Podman."""
|
|
20
|
+
|
|
21
|
+
DEFAULT_RUNTIME = "auto"
|
|
22
|
+
DEFAULT_PLATFORM = "linux/arm64"
|
|
23
|
+
|
|
24
|
+
def __init__(self, runtime_type: Optional[str] = None):
|
|
25
|
+
"""Initialize container runtime.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
runtime_type: Runtime type to use, defaults to auto-detection
|
|
29
|
+
"""
|
|
30
|
+
runtime_type = runtime_type or self.DEFAULT_RUNTIME
|
|
31
|
+
self.available_runtimes = ["finch", "docker", "podman"]
|
|
32
|
+
|
|
33
|
+
if runtime_type == "auto":
|
|
34
|
+
for runtime in self.available_runtimes:
|
|
35
|
+
if self._is_runtime_installed(runtime):
|
|
36
|
+
self.runtime = runtime
|
|
37
|
+
break
|
|
38
|
+
else:
|
|
39
|
+
raise RuntimeError("No container runtime found. Please install Docker, Finch, or Podman.")
|
|
40
|
+
elif runtime_type in self.available_runtimes:
|
|
41
|
+
if self._is_runtime_installed(runtime_type):
|
|
42
|
+
self.runtime = runtime_type
|
|
43
|
+
else:
|
|
44
|
+
raise RuntimeError(f"{runtime_type.capitalize()} is not installed")
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(f"Unsupported runtime: {runtime_type}")
|
|
47
|
+
|
|
48
|
+
def _is_runtime_installed(self, runtime: str) -> bool:
|
|
49
|
+
"""Check if runtime is installed."""
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run([runtime, "version"], capture_output=True, check=False) # nosec B603
|
|
52
|
+
return result.returncode == 0
|
|
53
|
+
except (FileNotFoundError, OSError):
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def get_name(self) -> str:
|
|
57
|
+
"""Get runtime name."""
|
|
58
|
+
return self.runtime.capitalize()
|
|
59
|
+
|
|
60
|
+
def image_exists(self, tag: str) -> bool:
|
|
61
|
+
"""Check if image exists."""
|
|
62
|
+
try:
|
|
63
|
+
result = subprocess.run([self.runtime, "images", "-q", tag], capture_output=True, text=True, check=False) # nosec B603
|
|
64
|
+
return bool(result.stdout.strip())
|
|
65
|
+
except (subprocess.SubprocessError, OSError):
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def generate_dockerfile(
|
|
69
|
+
self,
|
|
70
|
+
agent_path: Path,
|
|
71
|
+
output_dir: Path,
|
|
72
|
+
agent_name: str,
|
|
73
|
+
aws_region: Optional[str] = None,
|
|
74
|
+
enable_observability: bool = True,
|
|
75
|
+
requirements_file: Optional[str] = None,
|
|
76
|
+
) -> Path:
|
|
77
|
+
"""Generate Dockerfile from template."""
|
|
78
|
+
current_platform = self._get_current_platform()
|
|
79
|
+
required_platform = self.DEFAULT_PLATFORM
|
|
80
|
+
|
|
81
|
+
if current_platform != required_platform:
|
|
82
|
+
_handle_warn(
|
|
83
|
+
f"[WARNING] Platform mismatch: Current system is '{current_platform}' "
|
|
84
|
+
f"but Bedrock AgentCore requires '{required_platform}'.\n"
|
|
85
|
+
"For deployment options and workarounds, see: "
|
|
86
|
+
"https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/getting-started-custom.html\n"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
template_path = Path(__file__).parent / "templates" / "Dockerfile.j2"
|
|
90
|
+
|
|
91
|
+
if not template_path.exists():
|
|
92
|
+
log.error("Dockerfile template not found: %s", template_path)
|
|
93
|
+
raise FileNotFoundError(f"Dockerfile template not found: {template_path}")
|
|
94
|
+
|
|
95
|
+
with open(template_path) as f:
|
|
96
|
+
template = Template(f.read())
|
|
97
|
+
|
|
98
|
+
# Generate .dockerignore if it doesn't exist
|
|
99
|
+
self._ensure_dockerignore(output_dir)
|
|
100
|
+
|
|
101
|
+
# Validate module path before generating Dockerfile
|
|
102
|
+
self._validate_module_path(agent_path, output_dir)
|
|
103
|
+
|
|
104
|
+
# Calculate module path relative to project root
|
|
105
|
+
agent_module_path = self._get_module_path(agent_path, output_dir)
|
|
106
|
+
|
|
107
|
+
wheelhouse_dir = output_dir / "wheelhouse"
|
|
108
|
+
|
|
109
|
+
# Detect dependencies using the new DependencyInfo class
|
|
110
|
+
deps = detect_dependencies(output_dir, explicit_file=requirements_file)
|
|
111
|
+
|
|
112
|
+
# Add logic to avoid duplicate installation
|
|
113
|
+
has_current_package = False
|
|
114
|
+
if (output_dir / "pyproject.toml").exists():
|
|
115
|
+
# Only install current package if deps isn't already pointing to it
|
|
116
|
+
if not (deps.found and deps.is_root_package):
|
|
117
|
+
has_current_package = True
|
|
118
|
+
|
|
119
|
+
context = {
|
|
120
|
+
"python_version": get_python_version(),
|
|
121
|
+
"agent_file": agent_path.name,
|
|
122
|
+
"agent_module": agent_path.stem,
|
|
123
|
+
"agent_module_path": agent_module_path,
|
|
124
|
+
"agent_var": agent_name,
|
|
125
|
+
"has_wheelhouse": wheelhouse_dir.exists() and wheelhouse_dir.is_dir(),
|
|
126
|
+
"has_current_package": has_current_package,
|
|
127
|
+
"dependencies_file": deps.file,
|
|
128
|
+
"dependencies_install_path": deps.install_path,
|
|
129
|
+
"aws_region": aws_region,
|
|
130
|
+
"system_packages": [],
|
|
131
|
+
"observability_enabled": enable_observability,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
dockerfile_path = output_dir / "Dockerfile"
|
|
135
|
+
dockerfile_path.write_text(template.render(**context))
|
|
136
|
+
return dockerfile_path
|
|
137
|
+
|
|
138
|
+
def _ensure_dockerignore(self, project_dir: Path) -> None:
|
|
139
|
+
"""Create .dockerignore if it doesn't exist."""
|
|
140
|
+
dockerignore_path = project_dir / ".dockerignore"
|
|
141
|
+
if not dockerignore_path.exists():
|
|
142
|
+
template_path = Path(__file__).parent / "templates" / "dockerignore.template"
|
|
143
|
+
if template_path.exists():
|
|
144
|
+
dockerignore_path.write_text(template_path.read_text())
|
|
145
|
+
log.info("Generated .dockerignore")
|
|
146
|
+
|
|
147
|
+
def _validate_module_path(self, agent_path: Path, project_root: Path) -> None:
|
|
148
|
+
"""Validate that the agent path can be converted to a valid Python module path."""
|
|
149
|
+
try:
|
|
150
|
+
agent_path = agent_path.resolve()
|
|
151
|
+
relative_path = agent_path.relative_to(project_root)
|
|
152
|
+
for part in relative_path.parts[:-1]: # Check all directory parts
|
|
153
|
+
if "-" in part:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Directory name '{part}' contains hyphens which are not valid in Python module paths. "
|
|
156
|
+
f"Please rename '{part}' to '{part.replace('-', '_')}' or move your agent file to a "
|
|
157
|
+
f"directory with valid Python identifiers."
|
|
158
|
+
)
|
|
159
|
+
except ValueError as e:
|
|
160
|
+
if "does not start with" in str(e):
|
|
161
|
+
raise ValueError("Entrypoint file must be within the current project directory") from e
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
def _get_module_path(self, agent_path: Path, project_root: Path) -> str:
|
|
165
|
+
"""Get the Python module path for the agent file."""
|
|
166
|
+
try:
|
|
167
|
+
agent_path = agent_path.resolve()
|
|
168
|
+
# Get relative path from project root
|
|
169
|
+
relative_path = agent_path.relative_to(project_root)
|
|
170
|
+
# Convert to module path (e.g., src/agents/my_agent.py -> src.agents.my_agent)
|
|
171
|
+
parts = list(relative_path.parts[:-1]) + [relative_path.stem]
|
|
172
|
+
module_path = ".".join(parts)
|
|
173
|
+
|
|
174
|
+
# Handle notebook-generated handlers that start with .bedrock_agentcore
|
|
175
|
+
if module_path.startswith(".bedrock_agentcore"):
|
|
176
|
+
# Remove leading dot to make it a valid Python import
|
|
177
|
+
module_path = module_path[1:]
|
|
178
|
+
|
|
179
|
+
return module_path
|
|
180
|
+
except ValueError:
|
|
181
|
+
# If agent is outside project root, just use the filename
|
|
182
|
+
return agent_path.stem
|
|
183
|
+
|
|
184
|
+
def _get_current_platform(self) -> str:
|
|
185
|
+
"""Get the current system platform in standardized format."""
|
|
186
|
+
machine = platform.machine().lower()
|
|
187
|
+
arch_map = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}
|
|
188
|
+
arch = arch_map.get(machine, machine)
|
|
189
|
+
return f"linux/{arch}"
|
|
190
|
+
|
|
191
|
+
def build(self, dockerfile_dir: Path, tag: str, platform: Optional[str] = None) -> Tuple[bool, List[str]]:
|
|
192
|
+
"""Build container image."""
|
|
193
|
+
if not dockerfile_dir.exists():
|
|
194
|
+
return False, [f"Directory not found: {dockerfile_dir}"]
|
|
195
|
+
|
|
196
|
+
dockerfile_path = dockerfile_dir / "Dockerfile"
|
|
197
|
+
if not dockerfile_path.exists():
|
|
198
|
+
return False, [f"Dockerfile not found in {dockerfile_dir}"]
|
|
199
|
+
|
|
200
|
+
cmd = [self.runtime, "build", "-t", tag]
|
|
201
|
+
build_platform = platform or self.DEFAULT_PLATFORM
|
|
202
|
+
cmd.extend(["--platform", build_platform])
|
|
203
|
+
cmd.append(str(dockerfile_dir))
|
|
204
|
+
|
|
205
|
+
return self._execute_command(cmd)
|
|
206
|
+
|
|
207
|
+
def run_local(self, tag: str, port: int = 8080, env_vars: Optional[dict] = None) -> subprocess.CompletedProcess:
|
|
208
|
+
"""Run container locally.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
tag: Docker image tag to run
|
|
212
|
+
port: Port to expose (default: 8080)
|
|
213
|
+
env_vars: Additional environment variables to pass to container
|
|
214
|
+
"""
|
|
215
|
+
container_name = f"{tag.split(':')[0]}-{int(time.time())}"
|
|
216
|
+
cmd = [self.runtime, "run", "-it", "--rm", "-p", f"{port}:8080", "--name", container_name]
|
|
217
|
+
|
|
218
|
+
# Use boto3 to get current credentials
|
|
219
|
+
try:
|
|
220
|
+
import boto3
|
|
221
|
+
|
|
222
|
+
session = boto3.Session()
|
|
223
|
+
credentials = session.get_credentials()
|
|
224
|
+
|
|
225
|
+
if not credentials:
|
|
226
|
+
raise RuntimeError("No AWS credentials found. Please configure AWS credentials.")
|
|
227
|
+
|
|
228
|
+
# Get the frozen credentials (resolves temporary credentials too)
|
|
229
|
+
frozen_creds = credentials.get_frozen_credentials()
|
|
230
|
+
|
|
231
|
+
cmd.extend(["-e", f"AWS_ACCESS_KEY_ID={frozen_creds.access_key}"])
|
|
232
|
+
cmd.extend(["-e", f"AWS_SECRET_ACCESS_KEY={frozen_creds.secret_key}"])
|
|
233
|
+
|
|
234
|
+
if frozen_creds.token:
|
|
235
|
+
cmd.extend(["-e", f"AWS_SESSION_TOKEN={frozen_creds.token}"])
|
|
236
|
+
|
|
237
|
+
except ImportError:
|
|
238
|
+
raise RuntimeError("boto3 is required for local mode. Please install it.") from None
|
|
239
|
+
|
|
240
|
+
# Add additional environment variables if provided
|
|
241
|
+
if env_vars:
|
|
242
|
+
for key, value in env_vars.items():
|
|
243
|
+
cmd.extend(["-e", f"{key}={value}"])
|
|
244
|
+
|
|
245
|
+
cmd.append(tag)
|
|
246
|
+
return subprocess.run(cmd, check=False) # nosec B603
|
|
247
|
+
|
|
248
|
+
def login(self, registry: str, username: str, password: str) -> bool:
|
|
249
|
+
"""Login to registry."""
|
|
250
|
+
log.info("Authenticating with registry...")
|
|
251
|
+
try:
|
|
252
|
+
subprocess.run( # nosec B603
|
|
253
|
+
[self.runtime, "login", "--username", username, "--password-stdin", registry],
|
|
254
|
+
input=password.encode(),
|
|
255
|
+
capture_output=True,
|
|
256
|
+
check=True,
|
|
257
|
+
)
|
|
258
|
+
log.info("Registry authentication successful")
|
|
259
|
+
return True
|
|
260
|
+
except subprocess.CalledProcessError:
|
|
261
|
+
log.error("Registry authentication failed")
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
def tag(self, source: str, target: str) -> bool:
|
|
265
|
+
"""Tag an image."""
|
|
266
|
+
log.info("Tagging image: %s -> %s", source, target)
|
|
267
|
+
try:
|
|
268
|
+
subprocess.run([self.runtime, "tag", source, target], check=True) # nosec B603
|
|
269
|
+
return True
|
|
270
|
+
except subprocess.CalledProcessError:
|
|
271
|
+
log.error("Failed to tag image")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def push(self, tag: str) -> bool:
|
|
275
|
+
"""Push image to registry."""
|
|
276
|
+
log.info("Pushing image to registry...")
|
|
277
|
+
try:
|
|
278
|
+
subprocess.run([self.runtime, "push", tag], check=True) # nosec B603
|
|
279
|
+
log.info("Image pushed successfully")
|
|
280
|
+
return True
|
|
281
|
+
except subprocess.CalledProcessError:
|
|
282
|
+
log.error("Failed to push image")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def _execute_command(self, cmd: List[str]) -> Tuple[bool, List[str]]:
|
|
286
|
+
"""Execute command and capture output."""
|
|
287
|
+
try:
|
|
288
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) # nosec B603
|
|
289
|
+
|
|
290
|
+
output_lines = []
|
|
291
|
+
if process.stdout:
|
|
292
|
+
for line in process.stdout:
|
|
293
|
+
line = line.rstrip()
|
|
294
|
+
if line:
|
|
295
|
+
# Log output at source as it streams
|
|
296
|
+
if "error" in line.lower() or "failed" in line.lower():
|
|
297
|
+
log.error("Build: %s", line)
|
|
298
|
+
elif "Successfully" in line:
|
|
299
|
+
log.info("Build: %s", line)
|
|
300
|
+
else:
|
|
301
|
+
log.debug("Build: %s", line)
|
|
302
|
+
|
|
303
|
+
output_lines.append(line)
|
|
304
|
+
|
|
305
|
+
process.wait()
|
|
306
|
+
return process.returncode == 0, output_lines
|
|
307
|
+
|
|
308
|
+
except (subprocess.SubprocessError, OSError) as e:
|
|
309
|
+
log.error("Command execution failed: %s", str(e))
|
|
310
|
+
return False, [str(e)]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Bedrock AgentCore utility functions for parsing and importing Bedrock AgentCore applications."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_entrypoint(entrypoint: str) -> Tuple[Path, str]:
|
|
13
|
+
"""Parse entrypoint into file path and name.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
entrypoint: Entrypoint specification (e.g., "app.py")
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple of (file_path, bedrock_agentcore_name)
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If entrypoint cannot be parsed or file doesn't exist
|
|
23
|
+
"""
|
|
24
|
+
file_path = Path(entrypoint).resolve()
|
|
25
|
+
if not file_path.exists():
|
|
26
|
+
log.error("Entrypoint file not found: %s", file_path)
|
|
27
|
+
raise ValueError(f"File not found: {file_path}")
|
|
28
|
+
|
|
29
|
+
file_name = file_path.stem
|
|
30
|
+
|
|
31
|
+
log.info("Entrypoint parsed: file=%s, bedrock_agentcore_name=%s", file_path, file_name)
|
|
32
|
+
return file_path, file_name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handle_requirements_file(
|
|
36
|
+
requirements_file: Optional[str] = None, build_dir: Optional[Path] = None
|
|
37
|
+
) -> Optional[str]:
|
|
38
|
+
"""Handle requirements file selection and validation.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
requirements_file: Optional path to requirements file
|
|
42
|
+
build_dir: Build directory, defaults to current directory
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Validated requirements file path or None
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If specified requirements file is invalid
|
|
49
|
+
"""
|
|
50
|
+
if build_dir is None:
|
|
51
|
+
build_dir = Path.cwd()
|
|
52
|
+
|
|
53
|
+
if requirements_file:
|
|
54
|
+
log.info("Validating requirements file: %s", requirements_file)
|
|
55
|
+
# Validate provided requirements file
|
|
56
|
+
try:
|
|
57
|
+
deps = validate_requirements_file(build_dir, requirements_file)
|
|
58
|
+
log.info("Requirements file validated: %s", requirements_file)
|
|
59
|
+
return requirements_file
|
|
60
|
+
except (FileNotFoundError, ValueError) as e:
|
|
61
|
+
log.error("Requirements file validation failed: %s", e)
|
|
62
|
+
raise ValueError(str(e)) from e
|
|
63
|
+
|
|
64
|
+
# Auto-detect dependencies (no validation needed, just detection)
|
|
65
|
+
log.info("Auto-detecting dependencies in: %s", build_dir)
|
|
66
|
+
deps = detect_dependencies(build_dir)
|
|
67
|
+
|
|
68
|
+
if deps.found:
|
|
69
|
+
log.info("Dependencies detected: %s", deps.file)
|
|
70
|
+
return None # Let operations handle the detected file
|
|
71
|
+
else:
|
|
72
|
+
log.info("No dependency files found")
|
|
73
|
+
return None # No file found or specified
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class DependencyInfo:
|
|
78
|
+
"""Information about project dependencies."""
|
|
79
|
+
|
|
80
|
+
file: Optional[str] # Relative path for Docker context
|
|
81
|
+
type: str # "requirements", "pyproject", or "notfound"
|
|
82
|
+
resolved_path: Optional[str] = None # Absolute path for validation
|
|
83
|
+
install_path: Optional[str] = None # Path for pip install command
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def found(self) -> bool:
|
|
87
|
+
"""Whether a dependency file was found."""
|
|
88
|
+
return self.file is not None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def is_pyproject(self) -> bool:
|
|
92
|
+
"""Whether this is a pyproject.toml file."""
|
|
93
|
+
return self.type == "pyproject"
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def is_requirements(self) -> bool:
|
|
97
|
+
"""Whether this is a requirements file."""
|
|
98
|
+
return self.type == "requirements"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_root_package(self) -> bool:
|
|
102
|
+
"""Whether this dependency points to the root package."""
|
|
103
|
+
return self.is_pyproject and self.install_path == "."
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def detect_dependencies(package_dir: Path, explicit_file: Optional[str] = None) -> DependencyInfo:
|
|
107
|
+
"""Detect dependency file, with optional explicit override."""
|
|
108
|
+
if explicit_file:
|
|
109
|
+
return _handle_explicit_file(package_dir, explicit_file)
|
|
110
|
+
|
|
111
|
+
# Check for requirements.txt first (prioritized for notebook workflows)
|
|
112
|
+
requirements_path = package_dir / "requirements.txt"
|
|
113
|
+
if requirements_path.exists():
|
|
114
|
+
return DependencyInfo(
|
|
115
|
+
file="requirements.txt", type="requirements", resolved_path=str(requirements_path.resolve())
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Check for pyproject.toml
|
|
119
|
+
pyproject_path = package_dir / "pyproject.toml"
|
|
120
|
+
if pyproject_path.exists():
|
|
121
|
+
return DependencyInfo(
|
|
122
|
+
file="pyproject.toml",
|
|
123
|
+
type="pyproject",
|
|
124
|
+
resolved_path=str(pyproject_path.resolve()),
|
|
125
|
+
install_path=".", # Install from current directory
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return DependencyInfo(file=None, type="notfound")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _handle_explicit_file(package_dir: Path, explicit_file: str) -> DependencyInfo:
|
|
132
|
+
"""Handle explicitly provided dependency file."""
|
|
133
|
+
# Handle both absolute and relative paths
|
|
134
|
+
explicit_path = Path(explicit_file)
|
|
135
|
+
if not explicit_path.is_absolute():
|
|
136
|
+
explicit_path = package_dir / explicit_path
|
|
137
|
+
|
|
138
|
+
# Resolve the path to handle .. and . components
|
|
139
|
+
explicit_path = explicit_path.resolve()
|
|
140
|
+
|
|
141
|
+
if not explicit_path.exists():
|
|
142
|
+
raise FileNotFoundError(f"Specified requirements file not found: {explicit_path}")
|
|
143
|
+
|
|
144
|
+
# Ensure file is within project directory for Docker context
|
|
145
|
+
try:
|
|
146
|
+
relative_path = explicit_path.relative_to(package_dir.resolve())
|
|
147
|
+
file_path = str(relative_path)
|
|
148
|
+
except ValueError:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Requirements file must be within project directory. File: {explicit_path}, Project: {package_dir}"
|
|
151
|
+
) from None
|
|
152
|
+
|
|
153
|
+
# Determine type and install path
|
|
154
|
+
file_type = "requirements" if explicit_file.endswith((".txt", ".in")) else "pyproject"
|
|
155
|
+
install_path = None
|
|
156
|
+
|
|
157
|
+
if file_type == "pyproject":
|
|
158
|
+
if "/" in file_path:
|
|
159
|
+
# pyproject.toml in subdirectory - install from that directory
|
|
160
|
+
install_path = str(Path(file_path).parent)
|
|
161
|
+
else:
|
|
162
|
+
# pyproject.toml in root - install from current directory
|
|
163
|
+
install_path = "."
|
|
164
|
+
|
|
165
|
+
return DependencyInfo(file=file_path, type=file_type, resolved_path=str(explicit_path), install_path=install_path)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def validate_requirements_file(build_dir: Path, requirements_file: str) -> DependencyInfo:
|
|
169
|
+
"""Validate the provided requirements file path and return DependencyInfo."""
|
|
170
|
+
# Check if the provided path exists and is a file
|
|
171
|
+
file_path = Path(requirements_file)
|
|
172
|
+
if not file_path.is_absolute():
|
|
173
|
+
file_path = build_dir / file_path
|
|
174
|
+
|
|
175
|
+
if not file_path.exists():
|
|
176
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
177
|
+
|
|
178
|
+
if file_path.is_dir():
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Path is a directory, not a file: {file_path}. "
|
|
181
|
+
f"Please specify a requirements file (requirements.txt, pyproject.toml, etc.)"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Validate that it's a recognized dependency file type (flexible validation)
|
|
185
|
+
if not (file_path.suffix in [".txt", ".in"] or file_path.name == "pyproject.toml"):
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"'{file_path.name}' is not a supported dependency file. "
|
|
188
|
+
f"Supported formats: *.txt, *.in (pip requirements), or pyproject.toml"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Use the existing detect_dependencies function to process the file
|
|
192
|
+
return detect_dependencies(build_dir, explicit_file=requirements_file)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_python_version() -> str:
|
|
196
|
+
"""Get Python version for Docker image."""
|
|
197
|
+
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Utility functions for agent log information."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_agent_log_paths(agent_id: str, endpoint_name: Optional[str] = None) -> Tuple[str, str]:
|
|
7
|
+
"""Get CloudWatch log group paths for an agent.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
agent_id: The agent ID
|
|
11
|
+
endpoint_name: The endpoint name (defaults to "DEFAULT")
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Tuple of (runtime_log_group, otel_log_group)
|
|
15
|
+
"""
|
|
16
|
+
endpoint_name = endpoint_name or "DEFAULT"
|
|
17
|
+
runtime_log_group = f"/aws/bedrock-agentcore/runtimes/{agent_id}-{endpoint_name}"
|
|
18
|
+
otel_log_group = f"/aws/bedrock-agentcore/runtimes/{agent_id}-{endpoint_name}/runtime-logs"
|
|
19
|
+
return runtime_log_group, otel_log_group
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_aws_tail_commands(log_group: str) -> tuple[str, str]:
|
|
23
|
+
"""Get AWS CLI tail commands for a log group.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
log_group: The CloudWatch log group path
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (follow_command, since_command)
|
|
30
|
+
"""
|
|
31
|
+
follow_cmd = f"aws logs tail {log_group} --follow"
|
|
32
|
+
since_cmd = f"aws logs tail {log_group} --since 1h"
|
|
33
|
+
return follow_cmd, since_cmd
|