Skip to content

Commit bcdc6eb

Browse files
sundargthbSundar Raghavan
andauthored
feat(code-interpreter): Add convenience methods for file operations and package management (#202)
* feat(code-interpreter): Add convenience methods for file operations and package management --------- Co-authored-by: Sundar Raghavan <sdraghav@amazon.com>
1 parent b414068 commit bcdc6eb

3 files changed

Lines changed: 1240 additions & 21 deletions

File tree

src/bedrock_agentcore/tools/code_interpreter_client.py

Lines changed: 351 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
applications to start, stop, and invoke code execution in a managed sandbox environment.
55
"""
66

7+
import base64
78
import logging
89
import uuid
910
from contextlib import contextmanager
10-
from typing import Dict, Generator, Optional
11+
from typing import Any, Dict, Generator, List, Optional, Union
1112

1213
import boto3
14+
from botocore.config import Config
1315

1416
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
1517

@@ -31,6 +33,30 @@ class CodeInterpreter:
3133
client: The boto3 client for interacting with the service.
3234
identifier (str, optional): The code interpreter identifier.
3335
session_id (str, optional): The active session ID.
36+
37+
Basic Usage:
38+
>>> from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter
39+
>>>
40+
>>> client = CodeInterpreter('us-west-2')
41+
>>> client.start()
42+
>>>
43+
>>> # Execute code
44+
>>> result = client.execute_code("print('Hello, World!')")
45+
>>>
46+
>>> # Install packages
47+
>>> client.install_packages(['pandas', 'matplotlib'])
48+
>>>
49+
>>> # Upload and process data
50+
>>> client.upload_file('data.csv', csv_content, description='Sales data')
51+
>>>
52+
>>> client.stop()
53+
54+
Context Manager Usage:
55+
>>> from bedrock_agentcore.tools.code_interpreter_client import code_session
56+
>>>
57+
>>> with code_session('us-west-2') as client:
58+
... client.install_packages(['numpy'])
59+
... result = client.execute_code('import numpy as np; print(np.pi)')
3460
"""
3561

3662
def __init__(self, region: str, session: Optional[boto3.Session] = None) -> None:
@@ -58,10 +84,12 @@ def __init__(self, region: str, session: Optional[boto3.Session] = None) -> None
5884
"bedrock-agentcore",
5985
region_name=region,
6086
endpoint_url=get_data_plane_endpoint(region),
87+
config=Config(read_timeout=300),
6188
)
6289

6390
self._identifier = None
6491
self._session_id = None
92+
self._file_descriptions: Dict[str, str] = {}
6593

6694
@property
6795
def identifier(self) -> Optional[str]:
@@ -404,6 +432,328 @@ def invoke(self, method: str, params: Optional[Dict] = None):
404432
arguments=params or {},
405433
)
406434

435+
def upload_file(
436+
self,
437+
path: str,
438+
content: Union[str, bytes],
439+
description: str = "",
440+
) -> Dict[str, Any]:
441+
r"""Upload a file to the code interpreter environment.
442+
443+
This is a convenience wrapper around the writeFiles method that provides
444+
a cleaner interface for file uploads with optional semantic descriptions.
445+
446+
Args:
447+
path: Relative path where the file should be saved (e.g., 'data.csv',
448+
'scripts/analysis.py'). Must be relative to the working directory.
449+
Absolute paths starting with '/' are not allowed.
450+
content: File content as string (text files) or bytes (binary files).
451+
Binary content will be base64 encoded automatically.
452+
description: Optional semantic description of the file contents.
453+
This is stored as metadata and can help LLMs understand
454+
the data structure (e.g., "CSV with columns: date, revenue, product_id").
455+
456+
Returns:
457+
Dict containing the result of the write operation.
458+
459+
Raises:
460+
ValueError: If path is absolute or content type is invalid.
461+
462+
Example:
463+
>>> # Upload a CSV file
464+
>>> client.upload_file(
465+
... path='sales_data.csv',
466+
... content='date,revenue\n2024-01-01,1000\n2024-01-02,1500',
467+
... description='Daily sales data with columns: date, revenue'
468+
... )
469+
470+
>>> # Upload a Python script
471+
>>> client.upload_file(
472+
... path='scripts/analyze.py',
473+
... content='import pandas as pd\ndf = pd.read_csv("sales_data.csv")'
474+
... )
475+
"""
476+
if path.startswith("/"):
477+
raise ValueError(
478+
f"Path must be relative, not absolute. Got: {path}. Use paths like 'data.csv' or 'scripts/analysis.py'."
479+
)
480+
481+
# Handle binary content
482+
if isinstance(content, bytes):
483+
file_content = {"path": path, "blob": base64.b64encode(content).decode("utf-8")}
484+
else:
485+
file_content = {"path": path, "text": content}
486+
487+
if description:
488+
self.logger.info("Uploading file: %s (%s)", path, description)
489+
else:
490+
self.logger.info("Uploading file: %s", path)
491+
492+
result = self.invoke("writeFiles", {"content": [file_content]})
493+
494+
# Store description as metadata (available for future LLM context)
495+
if description:
496+
self._file_descriptions[path] = description
497+
498+
return result
499+
500+
def upload_files(
501+
self,
502+
files: List[Dict[str, str]],
503+
) -> Dict[str, Any]:
504+
"""Upload multiple files to the code interpreter environment.
505+
506+
This operation is atomic - either all files are written or none are.
507+
If any file fails, the entire operation fails.
508+
509+
Args:
510+
files: List of file specifications, each containing:
511+
- 'path': Relative file path
512+
- 'content': File content (string or bytes)
513+
- 'description': Optional semantic description
514+
515+
Returns:
516+
Dict containing the result of the write operation.
517+
518+
Example:
519+
>>> client.upload_files([
520+
... {'path': 'data.csv', 'content': csv_data, 'description': 'Sales data'},
521+
... {'path': 'config.json', 'content': json_config}
522+
... ])
523+
"""
524+
file_contents = []
525+
for file_spec in files:
526+
path = file_spec["path"]
527+
content = file_spec["content"]
528+
529+
if path.startswith("/"):
530+
raise ValueError(f"Path must be relative, not absolute. Got: {path}")
531+
532+
if isinstance(content, bytes):
533+
file_contents.append({"path": path, "blob": base64.b64encode(content).decode("utf-8")})
534+
else:
535+
file_contents.append({"path": path, "text": content})
536+
537+
self.logger.info("Uploading %d files", len(files))
538+
return self.invoke("writeFiles", {"content": file_contents})
539+
540+
def install_packages(
541+
self,
542+
packages: List[str],
543+
upgrade: bool = False,
544+
) -> Dict[str, Any]:
545+
"""Install Python packages in the code interpreter environment.
546+
547+
This is a convenience wrapper around executeCommand that handles
548+
pip install commands with proper formatting.
549+
550+
Args:
551+
packages: List of package names to install. Can include version
552+
specifiers (e.g., ['pandas>=2.0', 'numpy', 'scikit-learn==1.3.0']).
553+
upgrade: If True, adds --upgrade flag to update existing packages.
554+
555+
Returns:
556+
Dict containing the command execution result with stdout/stderr.
557+
558+
Example:
559+
>>> # Install multiple packages
560+
>>> client.install_packages(['pandas', 'matplotlib', 'scikit-learn'])
561+
562+
>>> # Install with version constraints
563+
>>> client.install_packages(['pandas>=2.0', 'numpy<2.0'])
564+
565+
>>> # Upgrade existing packages
566+
>>> client.install_packages(['pandas'], upgrade=True)
567+
"""
568+
if not packages:
569+
raise ValueError("At least one package name must be provided")
570+
571+
# Sanitize package names (basic validation)
572+
for pkg in packages:
573+
if any(char in pkg for char in [";", "&", "|", "`", "$"]):
574+
raise ValueError(f"Invalid characters in package name: {pkg}")
575+
576+
packages_str = " ".join(packages)
577+
upgrade_flag = "--upgrade " if upgrade else ""
578+
command = f"pip install {upgrade_flag}{packages_str}"
579+
580+
self.logger.info("Installing packages: %s", packages_str)
581+
return self.invoke("executeCommand", {"command": command})
582+
583+
def download_file(
584+
self,
585+
path: str,
586+
) -> str:
587+
"""Download/read a file from the code interpreter environment.
588+
589+
Args:
590+
path: Path to the file to read.
591+
592+
Returns:
593+
File content as string.
594+
595+
Raises:
596+
FileNotFoundError: If the file doesn't exist.
597+
598+
Example:
599+
>>> # Read a generated file
600+
>>> content = client.download_file('output/results.csv')
601+
>>> print(content)
602+
"""
603+
self.logger.info("Downloading file: %s", path)
604+
result = self.invoke("readFiles", {"paths": [path]})
605+
606+
# Parse the response to extract file content
607+
# Response structure from the API
608+
if "stream" in result:
609+
for event in result["stream"]:
610+
if "result" in event:
611+
for content_item in event["result"].get("content", []):
612+
if content_item.get("type") == "resource":
613+
resource = content_item.get("resource", {})
614+
if "text" in resource:
615+
return resource["text"]
616+
elif "blob" in resource:
617+
return base64.b64decode(resource["blob"]).decode("utf-8")
618+
619+
raise FileNotFoundError(f"Could not read file: {path}")
620+
621+
def download_files(
622+
self,
623+
paths: List[str],
624+
) -> Dict[str, str]:
625+
"""Download/read multiple files from the code interpreter environment.
626+
627+
Args:
628+
paths: List of file paths to read.
629+
630+
Returns:
631+
Dict mapping file paths to their contents.
632+
633+
Example:
634+
>>> files = client.download_files(['data.csv', 'results.json'])
635+
>>> print(files['data.csv'])
636+
"""
637+
self.logger.info("Downloading %d files", len(paths))
638+
result = self.invoke("readFiles", {"paths": paths})
639+
640+
files = {}
641+
if "stream" in result:
642+
for event in result["stream"]:
643+
if "result" in event:
644+
for content_item in event["result"].get("content", []):
645+
if content_item.get("type") == "resource":
646+
resource = content_item.get("resource", {})
647+
uri = resource.get("uri", "")
648+
file_path = uri.replace("file://", "")
649+
650+
if "text" in resource:
651+
files[file_path] = resource["text"]
652+
elif "blob" in resource:
653+
files[file_path] = base64.b64decode(resource["blob"]).decode("utf-8")
654+
655+
return files
656+
657+
def execute_code(
658+
self,
659+
code: str,
660+
language: str = "python",
661+
clear_context: bool = False,
662+
) -> Dict[str, Any]:
663+
"""Execute code in the interpreter environment.
664+
665+
This is a convenience wrapper around the executeCode method with
666+
typed parameters for better IDE support and validation.
667+
668+
Args:
669+
code: The code to execute.
670+
language: Programming language - 'python', 'javascript', or 'typescript'.
671+
Default is 'python'.
672+
clear_context: If True, clears all previous variable state before execution.
673+
Default is False (variables persist across calls).
674+
Note: Only supported for Python. Ignored for JavaScript/TypeScript.
675+
676+
Returns:
677+
Dict containing execution results including stdout, stderr, exit_code.
678+
679+
Example:
680+
>>> # Execute Python code
681+
>>> result = client.execute_code('''
682+
... import pandas as pd
683+
... df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
684+
... print(df.describe())
685+
... ''')
686+
687+
>>> # Clear context and start fresh
688+
>>> result = client.execute_code('x = 10', clear_context=True)
689+
"""
690+
valid_languages = ["python", "javascript", "typescript"]
691+
if language not in valid_languages:
692+
raise ValueError(f"Language must be one of {valid_languages}, got: {language}")
693+
694+
self.logger.info("Executing %s code (%d chars)", language, len(code))
695+
696+
return self.invoke(
697+
"executeCode",
698+
{
699+
"code": code,
700+
"language": language,
701+
"clearContext": clear_context,
702+
},
703+
)
704+
705+
def execute_command(
706+
self,
707+
command: str,
708+
) -> Dict[str, Any]:
709+
"""Execute a shell command in the interpreter environment.
710+
711+
This is a convenience wrapper around executeCommand.
712+
713+
Args:
714+
command: Shell command to execute.
715+
716+
Returns:
717+
Dict containing command execution results.
718+
719+
Example:
720+
>>> # List files
721+
>>> result = client.execute_command('ls -la')
722+
723+
>>> # Check Python version
724+
>>> result = client.execute_command('python --version')
725+
"""
726+
self.logger.info("Executing shell command: %s...", command[:50])
727+
return self.invoke("executeCommand", {"command": command})
728+
729+
def clear_context(self) -> Dict[str, Any]:
730+
"""Clear all variable state in the Python execution context.
731+
732+
This resets the interpreter to a fresh state, removing all
733+
previously defined variables, imports, and function definitions.
734+
735+
Note: Only affects Python context. JavaScript/TypeScript contexts
736+
are not affected.
737+
738+
Returns:
739+
Dict containing the result of the clear operation.
740+
741+
Example:
742+
>>> client.execute_code('x = 10')
743+
>>> client.execute_code('print(x)') # prints 10
744+
>>> client.clear_context()
745+
>>> client.execute_code('print(x)') # NameError: x is not defined
746+
"""
747+
self.logger.info("Clearing Python execution context")
748+
return self.invoke(
749+
"executeCode",
750+
{
751+
"code": "# Context cleared",
752+
"language": "python",
753+
"clearContext": True,
754+
},
755+
)
756+
407757

408758
@contextmanager
409759
def code_session(

0 commit comments

Comments
 (0)