1+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2+ # All rights reserved.
3+ #
4+ # This source code is licensed under the BSD-style license found in the
5+ # LICENSE file in the root directory of this source tree.
6+
7+ import json
8+ import uuid
9+ from typing import Any , Dict , Literal
10+
11+ from ..docker .docker_executor import DockerExecutor
12+ from .interfaces import Environment , Transform
13+ from .types import CodeAction , CodeObservation , CodeState , Action , Observation , State
14+
15+
16+ class CodeExecutionEnvironment (Environment ):
17+ """Environment for executing Python code actions using Docker."""
18+
19+ def __init__ (
20+ self ,
21+ transform : Transform | None = None ,
22+ docker_image : str = "python:3.11-slim" ,
23+ timeout_seconds : int = 30
24+ ):
25+ super ().__init__ (transform )
26+ self .docker_image = docker_image
27+ self .timeout_seconds = timeout_seconds
28+ self .executor = DockerExecutor (docker_image , timeout_seconds )
29+ self ._state = CodeState ()
30+
31+ def reset (self ) -> Observation :
32+ """Reset environment and start fresh Docker session."""
33+ # Stop any existing session
34+ self .executor .stop_session ()
35+
36+ # Initialize fresh state
37+ self ._state = CodeState (
38+ episode_id = str (uuid .uuid4 ()),
39+ step_count = 0
40+ )
41+
42+ # Start new Docker session
43+ try :
44+ self .executor .start_session ()
45+ except Exception as e :
46+ # Fail hard as requested
47+ raise RuntimeError (f"Failed to start Docker session: { e } " )
48+
49+ # Return initial observation
50+ observation = CodeObservation (
51+ execution_result = None ,
52+ available_tools = [] # TODO: populate from MCP registry
53+ )
54+
55+ return self ._apply_transform (observation )
56+
57+ def step (self , action : Action ) -> Observation :
58+ """Execute code action and return observation."""
59+ if not isinstance (action , CodeAction ):
60+ raise ValueError (f"Expected CodeAction, got { type (action )} " )
61+
62+ # Execute the code
63+ execution_result = self .executor .execute_code (action .code )
64+
65+ # Update state
66+ self ._state .step_count += 1
67+ self ._state .action_history .append (action )
68+ self ._state .result_history .append (execution_result )
69+
70+ # Create observation
71+ observation = CodeObservation (
72+ execution_result = execution_result ,
73+ available_tools = [] # TODO: populate from MCP registry
74+ )
75+
76+ return self ._apply_transform (observation )
77+
78+ def render (self , mode : Literal ["human" , "raw" , "ansi" ] = "human" ) -> Any :
79+ """Render current environment state."""
80+ try :
81+ variables = self .executor .get_variable_dump ()
82+ except Exception as e :
83+ variables = {"error" : f"Failed to get variables: { e } " }
84+
85+ render_data = {
86+ "episode_id" : self ._state .episode_id ,
87+ "step_count" : self ._state .step_count ,
88+ "variables" : variables ,
89+ "last_result" : self ._state .result_history [- 1 ] if self ._state .result_history else None
90+ }
91+
92+ if mode == "raw" :
93+ return render_data
94+ elif mode == "ansi" :
95+ return self ._render_ansi (render_data )
96+ else : # mode == "human"
97+ return self ._render_human (render_data )
98+
99+ def close (self ) -> None :
100+ """Close environment and clean up Docker container."""
101+ self .executor .stop_session ()
102+
103+ @property
104+ def state (self ) -> State :
105+ """Get current environment state."""
106+ return self ._state
107+
108+ def _render_human (self , data : Dict [str , Any ]) -> str :
109+ """Render in human-readable format."""
110+ lines = []
111+ lines .append (f"=== Code Environment (Episode: { data ['episode_id' ][:8 ]} ...) ===" )
112+ lines .append (f"Steps: { data ['step_count' ]} " )
113+
114+ if data .get ("last_result" ):
115+ result = data ["last_result" ]
116+ lines .append (f"Last execution: { '✓ Success' if result .success else '✗ Failed' } " )
117+ if result .stdout :
118+ lines .append (f"Output: { result .stdout [:100 ]} ..." )
119+ if not result .success and result .exception_message :
120+ lines .append (f"Error: { result .exception_message } " )
121+
122+ lines .append ("\n --- Variables ---" )
123+ variables = data .get ("variables" , {})
124+ if "error" in variables :
125+ lines .append (f"Error getting variables: { variables ['error' ]} " )
126+ else :
127+ for name , value in sorted (variables .items ()):
128+ lines .append (f"{ name } : { value } " )
129+
130+ return "\n " .join (lines )
131+
132+ def _render_ansi (self , data : Dict [str , Any ]) -> str :
133+ """Render in ANSI terminal format with colors."""
134+ lines = []
135+
136+ # ANSI color codes
137+ BLUE = "\033 [34m"
138+ GREEN = "\033 [32m"
139+ RED = "\033 [31m"
140+ YELLOW = "\033 [33m"
141+ RESET = "\033 [0m"
142+ BOLD = "\033 [1m"
143+
144+ lines .append (f"{ BOLD } { BLUE } === Code Environment ==={ RESET } " )
145+ lines .append (f"Episode: { data ['episode_id' ][:8 ]} ..." )
146+ lines .append (f"Steps: { YELLOW } { data ['step_count' ]} { RESET } " )
147+
148+ if data .get ("last_result" ):
149+ result = data ["last_result" ]
150+ status_color = GREEN if result .success else RED
151+ status_text = "Success" if result .success else "Failed"
152+ lines .append (f"Last execution: { status_color } { status_text } { RESET } " )
153+
154+ if result .stdout :
155+ lines .append (f"Output: { result .stdout [:100 ]} ..." )
156+ if not result .success and result .exception_message :
157+ lines .append (f"{ RED } Error: { result .exception_message } { RESET } " )
158+
159+ lines .append (f"\n { BOLD } --- Variables ---{ RESET } " )
160+ variables = data .get ("variables" , {})
161+ if "error" in variables :
162+ lines .append (f"{ RED } Error getting variables: { variables ['error' ]} { RESET } " )
163+ else :
164+ for name , value in sorted (variables .items ()):
165+ lines .append (f"{ YELLOW } { name } { RESET } : { value } " )
166+
167+ return "\n " .join (lines )
0 commit comments