Skip to content

Commit c9b6cd7

Browse files
committed
feat(tools): add code interpreter
1 parent fde2c53 commit c9b6cd7

File tree

5 files changed

+222
-1
lines changed

5 files changed

+222
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- **`utils/aws.py`**: AWS boto3 session caching utilities.
2222
- `get_boto3_session(region, profile_name)` with `lru_cache`
2323
- `get_assumed_role_session(role_arn, region)` with `RefreshableCredentials` for auto-refresh
24-
- Added `boto3`, `datasets`, `tqdm` to main dependencies.
24+
- **`tools/code_interpreter.py`**: `CodeInterpreterToolkit` for AWS Bedrock AgentCore Code Interpreter.
25+
- `execute_code` tool for running Python code
26+
- `execute_command` tool for running shell commands
27+
- Added `boto3`, `bedrock-agentcore`, `datasets`, `tqdm` to main dependencies.
2528

2629
## [0.0.2] - 2026-02-03
2730

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ The package lives in `src/strands_env/` with these modules:
7373

7474
**aws.py** — AWS boto3 session caching. `get_boto3_session(region, profile_name)` with `@lru_cache` (boto3 handles credential refresh). `get_assumed_role_session(role_arn, region)` uses `RefreshableCredentials` for programmatic role assumption with auto-refresh.
7575

76+
### `tools/`
77+
78+
**code_interpreter.py**`CodeInterpreterToolkit` wraps AWS Bedrock AgentCore Code Interpreter. Provides `execute_code` (Python) and `execute_command` (shell) tools. Sessions are lazily created and can be cleaned up via `cleanup()`.
79+
7680
### Key Design Decisions
7781

7882
- **Factory pattern**: `ModelFactory` returns lambdas (not Model instances) so each `step()` gets a fresh model with clean token tracking state.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies = [
2424
"strands-sglang>=0.1.2",
2525
"strands-agents-tools",
2626
"boto3>=1.26.0",
27+
"bedrock-agentcore",
2728
"transformers>=4.0.0,<5.0.0",
2829
"datasets",
2930
"math-verify>=0.8.0",

src/strands_env/tools/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2025 Horizon RL Contributors
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tools for `strands_env`."""
16+
17+
from .code_interpreter import CodeInterpreterToolkit
18+
19+
__all__ = [
20+
"CodeInterpreterToolkit",
21+
]
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Copyright 2025 Horizon RL Contributors
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Code sandbox toolkit using AWS Bedrock AgentCore Code Interpreter."""
16+
17+
from __future__ import annotations
18+
19+
from typing import TYPE_CHECKING, Any
20+
21+
from strands import tool
22+
23+
if TYPE_CHECKING:
24+
import boto3
25+
26+
CODE_INTERPRETER_ID = "aws.codeinterpreter.v1"
27+
28+
29+
class CodeInterpreterToolkit:
30+
"""Code toolkit using AWS Bedrock AgentCore Code Interpreter.
31+
32+
Provides `execute_code` and `execute_command` tools for running Python code
33+
and shell commands in a sandboxed environment.
34+
35+
Example:
36+
from strands_env.utils import get_boto3_session
37+
38+
session = get_boto3_session(region="us-east-1")
39+
toolkit = CodeInterpreterToolkit(boto3_session=session)
40+
41+
# In environment:
42+
class MyEnv(Environment):
43+
def __init__(self, boto3_session, ...):
44+
self.toolkit = CodeInterpreterToolkit(boto3_session)
45+
46+
def get_tools(self):
47+
return [self.toolkit.execute_code, self.toolkit.execute_command]
48+
49+
async def cleanup(self):
50+
self.toolkit.cleanup()
51+
"""
52+
53+
def __init__(
54+
self,
55+
boto3_session: boto3.Session,
56+
session_name: str = "strands-env-session",
57+
):
58+
"""Initialize the toolkit.
59+
60+
Args:
61+
boto3_session: boto3 session for AWS credentials.
62+
session_name: Name for the code interpreter session.
63+
"""
64+
self.region = boto3_session.region_name
65+
self.session_name = session_name
66+
self._client = boto3_session.client("bedrock-agentcore", region_name=self.region)
67+
self._session_id: str | None = None
68+
self._execute_code = self._create_execute_code_tool()
69+
self._execute_command = self._create_execute_command_tool()
70+
71+
def _get_session_id(self) -> str:
72+
"""Get or create a code interpreter session."""
73+
if self._session_id is None:
74+
response = self._client.start_code_interpreter_session(
75+
codeInterpreterIdentifier=CODE_INTERPRETER_ID,
76+
name=self.session_name,
77+
sessionTimeoutSeconds=3600,
78+
)
79+
self._session_id = response["sessionId"]
80+
return self._session_id
81+
82+
def _parse_stream_response(self, response: dict[str, Any]) -> str:
83+
"""Parse the EventStream response from invoke_code_interpreter.
84+
85+
Extracts text content from result events or error messages from exceptions.
86+
Returns plain text that strands will wrap in tool result format.
87+
88+
Args:
89+
response: Raw response from invoke_code_interpreter.
90+
91+
Returns:
92+
Text content from execution result or error message.
93+
"""
94+
errors: list[str] = []
95+
96+
for event in response.get("stream", []):
97+
if "result" in event:
98+
result = event["result"]
99+
content = result.get("content", [])
100+
# Extract text from content list
101+
if isinstance(content, list):
102+
texts = [c.get("text", "") for c in content if c.get("type") == "text"]
103+
return "\n".join(texts) if texts else str(content)
104+
return str(content)
105+
106+
# Check for exception events
107+
for error_key in (
108+
"accessDeniedException",
109+
"conflictException",
110+
"internalServerException",
111+
"resourceNotFoundException",
112+
"serviceQuotaExceededException",
113+
"throttlingException",
114+
"validationException",
115+
):
116+
if error_key in event:
117+
msg = event[error_key].get("message", error_key)
118+
errors.append(f"{error_key}: {msg}")
119+
break
120+
121+
# No result found - return collected errors or generic message
122+
return "\n".join(errors) if errors else "No result received"
123+
124+
def _create_execute_code_tool(self):
125+
"""Create execute_code tool."""
126+
client = self._client
127+
128+
@tool
129+
def execute_code(code: str) -> str:
130+
"""Execute Python code and return the result.
131+
132+
Args:
133+
code: The Python code to execute.
134+
135+
Returns:
136+
Execution output text or error message.
137+
"""
138+
response = client.invoke_code_interpreter(
139+
codeInterpreterIdentifier=CODE_INTERPRETER_ID,
140+
sessionId=self._get_session_id(),
141+
name="executeCode",
142+
arguments={"code": code, "language": "python"},
143+
)
144+
return self._parse_stream_response(response)
145+
146+
return execute_code
147+
148+
def _create_execute_command_tool(self):
149+
"""Create execute_command tool."""
150+
client = self._client
151+
152+
@tool
153+
def execute_command(command: str) -> str:
154+
"""Execute a shell command and return the result.
155+
156+
Args:
157+
command: The shell command to execute.
158+
159+
Returns:
160+
Execution output text or error message.
161+
"""
162+
response = client.invoke_code_interpreter(
163+
codeInterpreterIdentifier=CODE_INTERPRETER_ID,
164+
sessionId=self._get_session_id(),
165+
name="executeCommand",
166+
arguments={"command": command},
167+
)
168+
return self._parse_stream_response(response)
169+
170+
return execute_command
171+
172+
@property
173+
def execute_code(self):
174+
"""Get the execute_code tool."""
175+
return self._execute_code
176+
177+
@property
178+
def execute_command(self):
179+
"""Get the execute_command tool."""
180+
return self._execute_command
181+
182+
def cleanup(self) -> None:
183+
"""Clean up code interpreter session."""
184+
if self._session_id:
185+
try:
186+
self._client.stop_code_interpreter_session(
187+
codeInterpreterIdentifier=CODE_INTERPRETER_ID,
188+
sessionId=self._session_id,
189+
)
190+
except Exception:
191+
pass # Ignore cleanup errors
192+
self._session_id = None

0 commit comments

Comments
 (0)