@@ -40,13 +40,56 @@ def __init__(
4040 self .base_dir = Path (base_dir )
4141 self .max_workers = max_workers or multiprocessing .cpu_count () * 2
4242
43+ def _normalize_path (self , path : str ) -> str :
44+ """Normalize the path by:
45+ 1. Removing any file extensions (.prompt, .agent)
46+ 2. Converting backslashes to forward slashes
47+ 3. Removing leading and trailing slashes
48+ 4. Removing leading and trailing whitespace
49+ 5. Normalizing multiple consecutive slashes into a single forward slash
50+
51+ Args:
52+ path: The path to normalize
53+
54+ Returns:
55+ The normalized path
56+ """
57+ # Remove any file extensions
58+ path = path .rsplit ('.' , 1 )[0 ] if '.' in path else path
59+
60+ # Convert backslashes to forward slashes and normalize multiple slashes
61+ path = path .replace ('\\ ' , '/' )
62+
63+ # Remove leading/trailing whitespace and slashes
64+ path = path .strip ().strip ('/' )
65+
66+ # Normalize multiple consecutive slashes into a single forward slash
67+ while '//' in path :
68+ path = path .replace ('//' , '/' )
69+
70+ return path
71+
72+ def is_file (self , path : str ) -> bool :
73+ """Check if the path is a file by checking for .prompt or .agent extension.
74+
75+ Args:
76+ path: The path to check
77+
78+ Returns:
79+ True if the path ends with .prompt or .agent, False otherwise
80+ """
81+ return path .endswith ('.prompt' ) or path .endswith ('.agent' )
82+
4383 def _save_serialized_file (self , serialized_content : str , file_path : str , file_type : FileType ) -> None :
4484 """Save serialized file to local filesystem.
4585
4686 Args:
4787 serialized_content: The content to save
4888 file_path: The path where to save the file
4989 file_type: The type of file (prompt or agent)
90+
91+ Raises:
92+ Exception: If there is an error saving the file
5093 """
5194 try :
5295 # Create full path including base_dir prefix
@@ -65,26 +108,44 @@ def _save_serialized_file(self, serialized_content: str, file_path: str, file_ty
65108 logger .error (f"Failed to sync { file_type } { file_path } : { str (e )} " )
66109 raise
67110
68- def pull (self ,
111+ def _pull_file (self , path : str , environment : str | None = None ) -> None :
112+ """Pull a specific file from Humanloop to local filesystem.
113+
114+ Args:
115+ path: The path of the file without the extension (e.g. "path/to/file")
116+ environment: The environment to pull the file from
117+
118+ Raises:
119+ ValueError: If the file type is not supported
120+ Exception: If there is an error pulling the file
121+ """
122+ file = self .client .files .retrieve_by_path (
123+ path ,
124+ environment = environment ,
125+ include_content = True
126+ )
127+
128+ if file .type not in ["prompt" , "agent" ]:
129+ raise ValueError (f"Unsupported file type: { file .type } " )
130+
131+ self ._save_serialized_file (file .content , file .path , file .type )
132+
133+ def _pull_directory (self ,
134+ path : str | None = None ,
69135 environment : str | None = None ,
70- directory : str | None = None ,
71- path : str | None = None ,
72136 ) -> List [str ]:
73137 """Sync prompt and agent files from Humanloop to local filesystem.
74138
75- If `path` is provided, only the file at that path will be pulled.
76- If `directory` is provided, all files in that directory will be pulled (if both `path` and `directory` are provided, `path` will take precedence).
139+ If `path` is provided, only the files under that path will be pulled.
77140 If `environment` is provided, the files will be pulled from that environment.
78141
79142 Args:
80- environment: The environment to pull the files from.
81- directory: The directory to pull the files from.
82- path: The path of a specific file to pull from.
143+ path: The path of the directory to pull from (e.g. "path/to/directory")
144+ environment: The environment to pull the files from
83145
84146 Returns:
85147 List of successfully processed file paths
86148 """
87-
88149 successful_files = []
89150 failed_files = []
90151 page = 1
@@ -95,7 +156,8 @@ def pull(self,
95156 type = ["prompt" , "agent" ],
96157 page = page ,
97158 include_content = True ,
98- environment = environment
159+ environment = environment ,
160+ directory = path
99161 )
100162
101163 if len (response .records ) == 0 :
@@ -109,7 +171,7 @@ def pull(self,
109171 continue
110172
111173 if not file .path .startswith (path ):
112- # Filter by path
174+ # Filter by path
113175 continue
114176
115177 # Skip if no content
@@ -135,4 +197,24 @@ def pull(self,
135197 if failed_files :
136198 logger .error (f"Failed to sync { len (failed_files )} files" )
137199
138- return successful_files
200+ return successful_files
201+
202+ def pull (self , path : str , environment : str | None = None ) -> List [str ]:
203+ """Pull files from Humanloop to local filesystem.
204+
205+ If the path ends with .prompt or .agent, pulls that specific file.
206+ Otherwise, pulls all files under the specified directory path.
207+
208+ Args:
209+ path: The path to pull from (either a specific file or directory)
210+ environment: The environment to pull from
211+
212+ Returns:
213+ List of successfully processed file paths
214+ """
215+ normalized_path = self ._normalize_path (path )
216+ if self .is_file (path ):
217+ self ._pull_file (normalized_path , environment )
218+ return [path ]
219+ else :
220+ return self ._pull_directory (normalized_path , environment )
0 commit comments