agentrun-sdk 0.1.2__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 agentrun-sdk might be problematic. Click here for more details.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. agentrun_wrapper/tools/code_interpreter_client.py +186 -0
@@ -0,0 +1,152 @@
1
+ import uuid
2
+ import json
3
+ import logging
4
+ import requests
5
+ from typing import Any, Dict
6
+ from rich.console import Console
7
+
8
+ logger = logging.getLogger(__name__)
9
+ console = Console()
10
+
11
+ def generate_session_id() -> str:
12
+ """Generate session ID."""
13
+ return str(uuid.uuid4())
14
+
15
+ def _handle_http_response(response) -> dict:
16
+ response.raise_for_status()
17
+ if "text/event-stream" in response.headers.get("content-type", ""):
18
+ return _handle_streaming_response(response)
19
+ else:
20
+ # Check if response has content
21
+ if not response.content:
22
+ raise ValueError("Empty response from agent endpoint")
23
+
24
+ return {"response": response.text}
25
+
26
+ def get_data_plane_endpoint(region: str) -> str:
27
+ """Get the data plane endpoint URL for AgentRun services.
28
+
29
+ Args:
30
+ region: HW Cloud region to use.
31
+
32
+ Returns:
33
+ The data plane endpoint URL, either from environment override or constructed URL.
34
+ """
35
+ return f"https://agentrun.{region}.huaweicloud.com"
36
+
37
+ def _handle_streaming_response(response) -> Dict[str, Any]:
38
+ complete_text = ""
39
+ for line in response.iter_lines(chunk_size=1):
40
+ if line:
41
+ line = line.decode("utf-8")
42
+ if line.startswith("data: "):
43
+ json_chunk = line[6:]
44
+ try:
45
+ parsed_chunk = json.loads(json_chunk)
46
+ if isinstance(parsed_chunk, str):
47
+ text_chunk = parsed_chunk
48
+ else:
49
+ text_chunk = json.dumps(parsed_chunk, ensure_ascii=False)
50
+ text_chunk += "\n"
51
+ console.print(text_chunk, end="", style="bold cyan")
52
+ complete_text += text_chunk
53
+ except json.JSONDecodeError:
54
+ console.print(json_chunk, style="bold cyan")
55
+ continue
56
+ console.print()
57
+ return {}
58
+
59
+ class AgentRunClient:
60
+ def __init__(self, region: str):
61
+ """Initialize AgentRunClient.
62
+
63
+ Args:
64
+ region: HW Cloud region for the client
65
+ """
66
+ self.region = region
67
+ self.dp_endpoint = get_data_plane_endpoint(region)
68
+ self.logger = logging.getLogger(f"agentrun.http_runtime.{region}")
69
+
70
+ self.logger.debug("Initializing HTTP AgentRun client for region: %s", region)
71
+ self.logger.debug("Data plane: %s", self.dp_endpoint)
72
+
73
+ def invoke_endpoint(
74
+ self,
75
+ agent_id,
76
+ payload,
77
+ session_id: str,
78
+ endpoint_name: str = "DEFAULT",
79
+ stream: bool = True,
80
+ ) -> Dict:
81
+ """Invoke agent endpoint using HTTP request.
82
+
83
+ Args:
84
+ agent_id: id of the agent
85
+ payload: Payload to send (dict or string)
86
+ session_id: Session ID for the request
87
+ endpoint_name: Endpoint name, defaults to "DEFAULT"
88
+ stream: Whether to stream the response, defaults to True
89
+
90
+ Returns:
91
+ Response from the agent endpoint
92
+ """
93
+ url = f"{self.dp_endpoint}/runtimes/invocations"
94
+ # Headers
95
+ headers = {
96
+ "Content-Type": "application/json",
97
+ "AgentRun-Runtime-Session-Id": session_id,
98
+ }
99
+ try:
100
+ body = json.loads(payload) if isinstance(payload, str) else payload
101
+ except json.JSONDecodeError:
102
+ # Fallback for non-JSON strings - wrap in payload object
103
+ self.logger.warning("Failed to parse payload as JSON, wrapping in payload object")
104
+ body = {"payload": payload}
105
+ try:
106
+ # Make request with timeout
107
+ response = requests.post(
108
+ url,
109
+ params={"qualifier": endpoint_name},
110
+ headers=headers,
111
+ json=body,
112
+ timeout=900,
113
+ stream=stream,
114
+ )
115
+ return _handle_http_response(response, stream)
116
+ except requests.exceptions.RequestException as e:
117
+ self.logger.error("Failed to invoke agent endpoint: %s", str(e))
118
+ raise
119
+
120
+ class LocalAgentRunClient:
121
+ """Local AgentRun client for invoking endpoints."""
122
+
123
+ def __init__(self, endpoint: str):
124
+ """Initialize the local client with the given endpoint."""
125
+ self.endpoint = endpoint
126
+ self.logger = logging.getLogger("agentrun.http_local")
127
+
128
+ def invoke_endpoint(self, session_id: str, payload: str, stream: bool):
129
+ """Invoke the endpoint with the given parameters."""
130
+ from agentrun_wrapper.runtime.models import SESSION_HEADER
131
+
132
+ url = f"{self.endpoint}/invocations"
133
+
134
+ headers = {
135
+ "Content-Type": "application/json",
136
+ SESSION_HEADER: session_id,
137
+ }
138
+
139
+ try:
140
+ body = json.loads(payload) if isinstance(payload, str) else payload
141
+ except json.JSONDecodeError:
142
+ # Fallback for non-JSON strings - wrap in payload object
143
+ self.logger.warning("Failed to parse payload as JSON, wrapping in payload object")
144
+ body = {"payload": payload}
145
+
146
+ try:
147
+ # Make request with timeout
148
+ response = requests.post(url, headers=headers, json=body, timeout=900, stream=stream)
149
+ return _handle_http_response(response)
150
+ except requests.exceptions.RequestException as e:
151
+ self.logger.error("Failed to invoke agent endpoint: %s", str(e))
152
+ raise
@@ -0,0 +1,72 @@
1
+ """Centralized logging configuration for bedrock-agentcore-starter-toolkit."""
2
+
3
+ import logging
4
+
5
+ _LOGGING_CONFIGURED = False
6
+
7
+
8
+ def setup_toolkit_logging(mode: str = "sdk") -> None:
9
+ """Setup logging for bedrock-agentcore-starter-toolkit.
10
+
11
+ Args:
12
+ mode: "cli" or "sdk" (defaults to "sdk")
13
+ """
14
+ global _LOGGING_CONFIGURED
15
+ if _LOGGING_CONFIGURED:
16
+ return # Already configured, prevent duplicates
17
+
18
+ if mode == "cli":
19
+ _setup_cli_logging()
20
+ elif mode == "sdk":
21
+ _setup_sdk_logging()
22
+ else:
23
+ raise ValueError(f"Invalid logging mode: {mode}. Must be 'cli' or 'sdk'")
24
+
25
+ _LOGGING_CONFIGURED = True
26
+
27
+
28
+ def _setup_cli_logging() -> None:
29
+ """Setup logging for CLI usage with RichHandler."""
30
+ try:
31
+ from rich.logging import RichHandler
32
+
33
+ from ..cli.common import console
34
+
35
+ FORMAT = "%(message)s"
36
+ logging.basicConfig(
37
+ level="INFO",
38
+ format=FORMAT,
39
+ handlers=[RichHandler(show_time=False, show_path=False, show_level=False, console=console)],
40
+ force=True, # Override any existing configuration
41
+ )
42
+ except ImportError:
43
+ # Fallback if rich is not available
44
+ _setup_basic_logging()
45
+
46
+
47
+ def _setup_sdk_logging() -> None:
48
+ """Setup logging for SDK usage (notebooks, scripts, imports) with StreamHandler."""
49
+ # Configure logger for ALL toolkit modules (ensures all operation logs appear)
50
+ toolkit_logger = logging.getLogger("bedrock_agentcore_starter_toolkit")
51
+
52
+ if not toolkit_logger.handlers:
53
+ handler = logging.StreamHandler()
54
+ handler.setFormatter(logging.Formatter("%(message)s"))
55
+ toolkit_logger.addHandler(handler)
56
+ toolkit_logger.setLevel(logging.INFO)
57
+
58
+
59
+ def _setup_basic_logging() -> None:
60
+ """Setup basic logging as fallback."""
61
+ logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
62
+
63
+
64
+ def is_logging_configured() -> bool:
65
+ """Check if toolkit logging has been configured."""
66
+ return _LOGGING_CONFIGURED
67
+
68
+
69
+ def reset_logging_config() -> None:
70
+ """Reset logging configuration state (for testing)."""
71
+ global _LOGGING_CONFIGURED
72
+ _LOGGING_CONFIGURED = False
@@ -0,0 +1,94 @@
1
+ import yaml
2
+ import logging
3
+ from typing import Optional
4
+ from pathlib import Path
5
+ from .schema import AgentRunConfigSchema, AgentRunAgentSchema
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ def _is_legacy_format(data: dict) -> bool:
10
+ """Detect old single-agent format."""
11
+ return isinstance(data, dict) and "agents" not in data and "name" in data and "entrypoint" in data
12
+
13
+ def _transform_legacy_to_multi_agent(data: dict) -> AgentRunConfigSchema:
14
+ """Transform old format to new format at runtime."""
15
+ agent_config = AgentRunAgentSchema.model_validate(data)
16
+ return AgentRunConfigSchema(default_agent=agent_config.name, agents={agent_config.name: agent_config})
17
+
18
+ def load_config(config_path: Path) -> AgentRunConfigSchema:
19
+ """Load config with automatic legacy format transformation."""
20
+ if not config_path.exists():
21
+ raise FileNotFoundError(f"Configuration not found: {config_path}")
22
+
23
+ with open(config_path, "r") as f:
24
+ data = yaml.safe_load(f) or {}
25
+
26
+ # Auto-detect and transform legacy format
27
+ if _is_legacy_format(data):
28
+ return _transform_legacy_to_multi_agent(data)
29
+
30
+ # New format
31
+ try:
32
+ return AgentRunConfigSchema.model_validate(data)
33
+ except Exception as e:
34
+ raise ValueError(f"Invalid configuration format: {e}") from e
35
+
36
+ def load_config_if_exists(config_path: Path) -> Optional[AgentRunConfigSchema]:
37
+ """Load configuration if file exists, otherwise return None.
38
+
39
+ Args:
40
+ config_path: Path to configuration file
41
+
42
+ Returns:
43
+ BedrockAgentCoreConfigSchema instance or None if file doesn't exist
44
+ """
45
+ if not config_path.exists():
46
+ return None
47
+ return load_config(config_path)
48
+
49
+ def save_config(config: AgentRunConfigSchema, config_path: Path):
50
+ """Save configuration to YAML file.
51
+
52
+ Args:
53
+ config: BedrockAgentCoreConfigSchema instance to save
54
+ config_path: Path to save configuration file
55
+ """
56
+ with open(config_path, "w") as f:
57
+ yaml.dump(config.model_dump(), f, default_flow_style=False, sort_keys=False)
58
+
59
+ def merge_agent_config(
60
+ config_path: Path, agent_name: str, new_config: AgentRunConfigSchema
61
+ ) -> AgentRunConfigSchema:
62
+ """Merge agent configuration into config.
63
+
64
+ Args:
65
+ config_path: Path to configuration file
66
+ agent_name: Name of the agent to add/update
67
+ new_config: Agent configuration to merge
68
+
69
+ Returns:
70
+ Updated project configuration
71
+ """
72
+ config = load_config_if_exists(config_path)
73
+
74
+ # Handle None case - create new config
75
+ if config is None:
76
+ config = AgentRunConfigSchema()
77
+
78
+ # Add/update agent
79
+ config.agents[agent_name] = new_config
80
+
81
+ # Log default agent change and always set current agent as default
82
+ old_default = config.default_agent
83
+ if old_default != agent_name:
84
+ if old_default:
85
+ log.info("Changing default agent from '%s' to '%s'", old_default, agent_name)
86
+ else:
87
+ log.info("Setting '%s' as default agent", agent_name)
88
+ else:
89
+ log.info("Keeping '%s' as default agent", agent_name)
90
+
91
+ # Always set current agent as default (the agent being configured becomes the new default)
92
+ config.default_agent = agent_name
93
+
94
+ return config
@@ -0,0 +1,280 @@
1
+ import logging
2
+ import subprocess
3
+ import time
4
+ import platform
5
+ from jinja2 import Template
6
+ from pathlib import Path
7
+ from typing import List, Optional, Tuple
8
+ from ...cli.common import _handle_warn
9
+ from .entrypoint import detect_dependencies, get_python_version
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+ class ContainerRuntime:
14
+ DEFAULT_PLATFORM = "linux/amd64"
15
+ DEFAULT_RUNTIME = "auto"
16
+
17
+ def __init__(self, runtime_type: Optional[str] = None):
18
+ runtime_type = runtime_type or self.DEFAULT_RUNTIME
19
+ self.available_runtimes = ["docker"]
20
+ self.has_local_runtime = False
21
+ self.runtime = None
22
+
23
+ if runtime_type == "auto":
24
+ for runtime in self.available_runtimes:
25
+ if self._is_runtime_installed(runtime):
26
+ self.runtime = runtime
27
+ self.has_local_runtime = True
28
+ break
29
+ else:
30
+ # Informational message - default CodeBuild deployment works fine
31
+ _handle_warn(
32
+ "ℹ️ No container engine found (Docker/Finch/Podman not installed)\n"
33
+ "✅ Default deployment uses CodeBuild (no container engine needed)\n"
34
+ "💡 Run 'agentcore launch' for cloud-based building and deployment\n"
35
+ "💡 For local builds, install Docker, Finch, or Podman"
36
+ )
37
+ self.runtime = "none"
38
+ self.has_local_runtime = False
39
+ elif runtime_type in self.available_runtimes:
40
+ if self._is_runtime_installed(runtime_type):
41
+ self.runtime = runtime_type
42
+ self.has_local_runtime = True
43
+ else:
44
+ # Convert hard error to warning - suggest CodeBuild instead
45
+ _handle_warn(
46
+ f"⚠️ {runtime_type.capitalize()} is not installed\n"
47
+ "💡 Recommendation: Use CodeBuild for building containers in the cloud\n"
48
+ "💡 Run 'agentcore launch' (default) for CodeBuild deployment\n"
49
+ f"💡 For local builds, please install {runtime_type.capitalize()}"
50
+ )
51
+ self.runtime = "none"
52
+ self.has_local_runtime = False
53
+ else:
54
+ if runtime_type == "none":
55
+ raise ValueError(
56
+ "No supported container engine found.\n\n"
57
+ "AgentCore requires one of the following container engines for local builds:\n"
58
+ "• Docker (any recent version, including Docker Desktop)\n"
59
+ "• Finch (Amazon's open-source container engine)\n"
60
+ "• Podman (compatible alternative to Docker)\n\n"
61
+ "To install:\n"
62
+ "• Docker: https://docs.docker.com/get-docker/\n"
63
+ "• Finch: https://github.com/runfinch/finch\n"
64
+ "• Podman: https://podman.io/getting-started/installation\n\n"
65
+ "Alternative: Use CodeBuild for cloud-based building (no container engine needed):\n"
66
+ " agentcore launch # Uses CodeBuild (default)"
67
+ )
68
+ else:
69
+ raise ValueError(f"Unsupported runtime: {runtime_type}")
70
+
71
+ def get_name(self) -> str:
72
+ """Get runtime name."""
73
+ return self.runtime.capitalize()
74
+
75
+ def _is_runtime_installed(self, runtime: str) -> bool:
76
+ """Check if runtime is installed."""
77
+ try:
78
+ result = subprocess.run([runtime, "version"], capture_output=True, check=False) # nosec B603
79
+ return result.returncode == 0
80
+ except (FileNotFoundError, OSError):
81
+ return False
82
+
83
+ def _get_current_platform(self) -> str:
84
+ """Get the current system platform in standardized format."""
85
+ machine = platform.machine().lower()
86
+ arch_map = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}
87
+ arch = arch_map.get(machine, machine)
88
+ return f"linux/{arch}"
89
+
90
+ def _ensure_dockerignore(self, project_dir: Path) -> None:
91
+ """Create .dockerignore if it doesn't exist."""
92
+ dockerignore_path = project_dir / ".dockerignore"
93
+ if not dockerignore_path.exists():
94
+ template_path = Path(__file__).parent / "templates" / "dockerignore.template"
95
+ if template_path.exists():
96
+ dockerignore_path.write_text(template_path.read_text())
97
+ log.info("Generated .dockerignore")
98
+
99
+ def _validate_module_path(self, agent_path: Path, project_root: Path) -> None:
100
+ """Validate that the agent path can be converted to a valid Python module path."""
101
+ try:
102
+ agent_path = agent_path.resolve()
103
+ relative_path = agent_path.relative_to(project_root)
104
+ for part in relative_path.parts[:-1]: # Check all directory parts
105
+ if "-" in part:
106
+ raise ValueError(
107
+ f"Directory name '{part}' contains hyphens which are not valid in Python module paths. "
108
+ f"Please rename '{part}' to '{part.replace('-', '_')}' or move your agent file to a "
109
+ f"directory with valid Python identifiers."
110
+ )
111
+ except ValueError as e:
112
+ if "does not start with" in str(e):
113
+ raise ValueError("Entrypoint file must be within the current project directory") from e
114
+ raise
115
+
116
+ def _get_module_path(self, agent_path: Path, project_root: Path) -> str:
117
+ """Get the Python module path for the agent file."""
118
+ try:
119
+ agent_path = agent_path.resolve()
120
+ # Get relative path from project root
121
+ relative_path = agent_path.relative_to(project_root)
122
+ # Convert to module path (e.g., src/agents/my_agent.py -> src.agents.my_agent)
123
+ parts = list(relative_path.parts[:-1]) + [relative_path.stem]
124
+ module_path = ".".join(parts)
125
+
126
+ # Handle notebook-generated handlers that start with .bedrock_agentcore
127
+ if module_path.startswith(".bedrock_agentcore"):
128
+ # Remove leading dot to make it a valid Python import
129
+ module_path = module_path[1:]
130
+
131
+ return module_path
132
+ except ValueError:
133
+ # If agent is outside project root, just use the filename
134
+ return agent_path.stem
135
+
136
+ def generate_dockerfile(
137
+ self,
138
+ agent_path: Path,
139
+ output_dir: Path,
140
+ agent_name: str,
141
+ hw_cloud_region: Optional[str] = None,
142
+ requirements_file: Optional[str] = None,
143
+ ) -> Path:
144
+ """Generate Dockerfile from template."""
145
+ current_platform = self._get_current_platform()
146
+ required_platform = self.DEFAULT_PLATFORM
147
+
148
+ if current_platform != required_platform:
149
+ _handle_warn(
150
+ f"[WARNING] Platform mismatch: Current system is '{current_platform}' "
151
+ f"but Bedrock AgentCore requires '{required_platform}'.\n"
152
+ "For deployment options and workarounds, see: "
153
+ "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/getting-started-custom.html\n"
154
+ )
155
+
156
+ template_path = Path(__file__).parent / "templates" / "Dockerfile.j2"
157
+
158
+ if not template_path.exists():
159
+ log.error("Dockerfile template not found: %s", template_path)
160
+ raise FileNotFoundError(f"Dockerfile template not found: {template_path}")
161
+
162
+ with open(template_path) as f:
163
+ template = Template(f.read())
164
+
165
+ # Generate .dockerignore if it doesn't exist
166
+ self._ensure_dockerignore(output_dir)
167
+
168
+ # Validate module path before generating Dockerfile
169
+ self._validate_module_path(agent_path, output_dir)
170
+
171
+ # Calculate module path relative to project root
172
+ agent_module_path = self._get_module_path(agent_path, output_dir)
173
+
174
+ wheelhouse_dir = output_dir / "wheelhouse"
175
+
176
+ # Detect dependencies using the new DependencyInfo class
177
+ deps = detect_dependencies(output_dir, explicit_file=requirements_file)
178
+
179
+ # Add logic to avoid duplicate installation
180
+ has_current_package = False
181
+ if (output_dir / "pyproject.toml").exists():
182
+ # Only install current package if deps isn't already pointing to it
183
+ if not (deps.found and deps.is_root_package):
184
+ has_current_package = True
185
+
186
+ context = {
187
+ "python_version": get_python_version(),
188
+ "agent_file": agent_path.name,
189
+ "agent_module": agent_path.stem,
190
+ "agent_module_path": agent_module_path,
191
+ "agent_var": agent_name,
192
+ "has_wheelhouse": wheelhouse_dir.exists() and wheelhouse_dir.is_dir(),
193
+ "has_current_package": has_current_package,
194
+ "dependencies_file": deps.file,
195
+ "dependencies_install_path": deps.install_path,
196
+ "hw_cloud_region": hw_cloud_region,
197
+ "system_packages": [],
198
+ }
199
+
200
+ dockerfile_path = output_dir / "Dockerfile"
201
+ dockerfile_path.write_text(template.render(**context))
202
+ return dockerfile_path
203
+
204
+ def build(self, dockerfile_dir: Path, tag: str, platform: Optional[str] = None) -> Tuple[bool, List[str]]:
205
+ """Build container image."""
206
+ if not self.has_local_runtime:
207
+ return False, [
208
+ "No container runtime available for local build",
209
+ "💡 Recommendation: Use CodeBuild for building containers in the cloud",
210
+ "💡 Run 'agentcore launch' (default) for CodeBuild deployment",
211
+ "💡 For local builds, please install Docker, Finch, or Podman",
212
+ ]
213
+
214
+ if not dockerfile_dir.exists():
215
+ return False, [f"Directory not found: {dockerfile_dir}"]
216
+
217
+ dockerfile_path = dockerfile_dir / "Dockerfile"
218
+ if not dockerfile_path.exists():
219
+ return False, [f"Dockerfile not found in {dockerfile_dir}"]
220
+
221
+ cmd = [self.runtime, "build", "-t", tag]
222
+ build_platform = platform or self.DEFAULT_PLATFORM
223
+ cmd.extend(["--platform", build_platform])
224
+ cmd.append(str(dockerfile_dir))
225
+
226
+ return self._execute_command(cmd)
227
+
228
+ def run_local(self, tag: str, port: int = 8080, env_vars: Optional[dict] = None) -> subprocess.CompletedProcess:
229
+ """Run container locally.
230
+
231
+ Args:
232
+ tag: Docker image tag to run
233
+ port: Port to expose (default: 8080)
234
+ env_vars: Additional environment variables to pass to container
235
+ """
236
+ if not self.has_local_runtime:
237
+ raise RuntimeError(
238
+ "No container runtime available for local run\n"
239
+ "💡 Recommendation: Use CodeBuild for building containers in the cloud\n"
240
+ "💡 Run 'agentcore launch' (default) for CodeBuild deployment\n"
241
+ "💡 For local runs, please install Docker, Finch, or Podman"
242
+ )
243
+
244
+ container_name = f"{tag.split(':')[0]}-{int(time.time())}"
245
+ cmd = [self.runtime, "run", "-it", "--rm", "-p", f"{port}:8080", "--name", container_name]
246
+
247
+ # Add additional environment variables if provided
248
+ if env_vars:
249
+ for key, value in env_vars.items():
250
+ cmd.extend(["-e", f"{key}={value}"])
251
+
252
+ cmd.append(tag)
253
+ return subprocess.run(cmd, check=False) # nosec B603
254
+
255
+ def _execute_command(self, cmd: List[str]) -> Tuple[bool, List[str]]:
256
+ """Execute command and capture output."""
257
+ try:
258
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) # nosec B603
259
+
260
+ output_lines = []
261
+ if process.stdout:
262
+ for line in process.stdout:
263
+ line = line.rstrip()
264
+ if line:
265
+ # Log output at source as it streams
266
+ if "error" in line.lower() or "failed" in line.lower():
267
+ log.error("Build: %s", line)
268
+ elif "Successfully" in line:
269
+ log.info("Build: %s", line)
270
+ else:
271
+ log.debug("Build: %s", line)
272
+
273
+ output_lines.append(line)
274
+
275
+ process.wait()
276
+ return process.returncode == 0, output_lines
277
+
278
+ except (subprocess.SubprocessError, OSError) as e:
279
+ log.error("Command execution failed: %s", str(e))
280
+ return False, [str(e)]