44applications to start, stop, and invoke code execution in a managed sandbox environment.
55"""
66
7+ import base64
78import logging
89import uuid
910from contextlib import contextmanager
10- from typing import Dict , Generator , Optional
11+ from typing import Any , Dict , Generator , List , Optional , Union
1112
1213import boto3
14+ from botocore .config import Config
1315
1416from 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
409759def code_session (
0 commit comments