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.

Files changed (50) hide show
  1. bedrock_agentcore_starter_toolkit/__init__.py +5 -0
  2. bedrock_agentcore_starter_toolkit/cli/cli.py +32 -0
  3. bedrock_agentcore_starter_toolkit/cli/common.py +44 -0
  4. bedrock_agentcore_starter_toolkit/cli/gateway/__init__.py +1 -0
  5. bedrock_agentcore_starter_toolkit/cli/gateway/commands.py +88 -0
  6. bedrock_agentcore_starter_toolkit/cli/runtime/__init__.py +1 -0
  7. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +651 -0
  8. bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +133 -0
  9. bedrock_agentcore_starter_toolkit/notebook/__init__.py +5 -0
  10. bedrock_agentcore_starter_toolkit/notebook/runtime/__init__.py +1 -0
  11. bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +239 -0
  12. bedrock_agentcore_starter_toolkit/operations/__init__.py +1 -0
  13. bedrock_agentcore_starter_toolkit/operations/gateway/README.md +277 -0
  14. bedrock_agentcore_starter_toolkit/operations/gateway/__init__.py +6 -0
  15. bedrock_agentcore_starter_toolkit/operations/gateway/client.py +456 -0
  16. bedrock_agentcore_starter_toolkit/operations/gateway/constants.py +152 -0
  17. bedrock_agentcore_starter_toolkit/operations/gateway/create_lambda.py +85 -0
  18. bedrock_agentcore_starter_toolkit/operations/gateway/create_role.py +90 -0
  19. bedrock_agentcore_starter_toolkit/operations/gateway/exceptions.py +13 -0
  20. bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +26 -0
  21. bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +241 -0
  22. bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +404 -0
  23. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +129 -0
  24. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +439 -0
  25. bedrock_agentcore_starter_toolkit/operations/runtime/models.py +79 -0
  26. bedrock_agentcore_starter_toolkit/operations/runtime/status.py +66 -0
  27. bedrock_agentcore_starter_toolkit/services/codebuild.py +332 -0
  28. bedrock_agentcore_starter_toolkit/services/ecr.py +84 -0
  29. bedrock_agentcore_starter_toolkit/services/runtime.py +473 -0
  30. bedrock_agentcore_starter_toolkit/utils/endpoints.py +32 -0
  31. bedrock_agentcore_starter_toolkit/utils/logging_config.py +72 -0
  32. bedrock_agentcore_starter_toolkit/utils/runtime/config.py +129 -0
  33. bedrock_agentcore_starter_toolkit/utils/runtime/container.py +310 -0
  34. bedrock_agentcore_starter_toolkit/utils/runtime/entrypoint.py +197 -0
  35. bedrock_agentcore_starter_toolkit/utils/runtime/logs.py +33 -0
  36. bedrock_agentcore_starter_toolkit/utils/runtime/policy_template.py +74 -0
  37. bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +151 -0
  38. bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +44 -0
  39. bedrock_agentcore_starter_toolkit/utils/runtime/templates/dockerignore.template +68 -0
  40. bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_policy.json.j2 +98 -0
  41. bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_trust_policy.json.j2 +21 -0
  42. bedrock_agentcore_starter_toolkit-0.1.1.dist-info/METADATA +137 -0
  43. bedrock_agentcore_starter_toolkit-0.1.1.dist-info/RECORD +47 -0
  44. bedrock_agentcore_starter_toolkit-0.1.1.dist-info/entry_points.txt +2 -0
  45. bedrock_agentcore_starter_toolkit-0.1.1.dist-info/licenses/NOTICE.txt +190 -0
  46. bedrock_agentcore_starter_toolkit/init.py +0 -3
  47. bedrock_agentcore_starter_toolkit-0.0.1.dist-info/METADATA +0 -26
  48. bedrock_agentcore_starter_toolkit-0.0.1.dist-info/RECORD +0 -5
  49. {bedrock_agentcore_starter_toolkit-0.0.1.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/WHEEL +0 -0
  50. /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