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.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- 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)]
|