1+ import os
2+ import logging
3+ from pathlib import Path
4+ import concurrent .futures
5+ from typing import List , TYPE_CHECKING , Union
6+
7+ from humanloop .types import FileType , PromptResponse , AgentResponse
8+ from humanloop .core .api_error import ApiError
9+
10+ if TYPE_CHECKING :
11+ from humanloop .base_client import BaseHumanloop
12+
13+ # Set up logging
14+ logger = logging .getLogger (__name__ )
15+ logger .setLevel (logging .INFO )
16+ console_handler = logging .StreamHandler ()
17+ formatter = logging .Formatter ("%(message)s" )
18+ console_handler .setFormatter (formatter )
19+ if not logger .hasHandlers ():
20+ logger .addHandler (console_handler )
21+
22+ def _save_serialized_file (serialized_content : str , file_path : str , file_type : FileType ) -> None :
23+ """Save serialized file to local filesystem.
24+
25+ :param serialized_content: The content to save
26+ :param file_path: The path where to save the file
27+ :param file_type: The type of file (prompt or agent)
28+ """
29+ try :
30+ # Create full path including humanloop/ prefix
31+ full_path = Path ("humanloop" ) / file_path
32+ # Create directory if it doesn't exist
33+ full_path .parent .mkdir (parents = True , exist_ok = True )
34+
35+ # Add file type extension
36+ new_path = full_path .parent / f"{ full_path .stem } .{ file_type } "
37+
38+ # Write content to file
39+ with open (new_path , "w" ) as f :
40+ f .write (serialized_content )
41+ logger .info (f"Syncing { file_type } { file_path } " )
42+ except Exception as e :
43+ logger .error (f"Failed to sync { file_type } { file_path } : { str (e )} " )
44+ raise
45+
46+ def _process_file (client : "BaseHumanloop" , file : Union [PromptResponse , AgentResponse ]) -> None :
47+ """Process a single file by serializing and saving it.
48+
49+ Currently only supports prompt and agent files. Other file types will be skipped.
50+
51+ :param client: Humanloop client instance
52+ :param file: The file to process (must be a PromptResponse or AgentResponse)
53+ """
54+ try :
55+ # Serialize the file based on its type
56+ try :
57+ if file .type == "prompt" :
58+ serialized = client .prompts .serialize (id = file .id )
59+ elif file .type == "agent" :
60+ serialized = client .agents .serialize (id = file .id )
61+ else :
62+ logger .warning (f"Skipping unsupported file type: { file .type } " )
63+ return
64+ except ApiError as e :
65+ # The SDK returns the YAML content in the error body when it can't parse as JSON
66+ if e .status_code == 200 :
67+ serialized = e .body
68+ else :
69+ raise
70+ except Exception as e :
71+ logger .error (f"Failed to serialize { file .type } { file .id } : { str (e )} " )
72+ raise
73+
74+ # Save to local filesystem
75+ _save_serialized_file (serialized , file .path , file .type )
76+
77+ except Exception as e :
78+ logger .error (f"Error processing file { file .path } : { str (e )} " )
79+ raise
80+
81+ def sync (client : "BaseHumanloop" ) -> List [str ]:
82+ """Sync prompt and agent files from Humanloop to local filesystem.
83+
84+ :param client: Humanloop client instance
85+ :return: List of successfully processed file paths
86+ """
87+ successful_files = []
88+ failed_files = []
89+
90+ # Create a thread pool for processing files
91+ with concurrent .futures .ThreadPoolExecutor (max_workers = 5 ) as executor :
92+ futures = []
93+ page = 1
94+
95+ while True :
96+ try :
97+ response = client .files .list_files (
98+ type = ["prompt" , "agent" ],
99+ page = page
100+ )
101+
102+ if len (response .records ) == 0 :
103+ break
104+
105+ # Submit each file for processing
106+ for file in response .records :
107+ future = executor .submit (_process_file , client , file )
108+ futures .append ((file .path , future ))
109+
110+ page += 1
111+ except Exception as e :
112+ logger .error (f"Failed to fetch page { page } : { str (e )} " )
113+ break
114+
115+ # Wait for all tasks to complete
116+ for file_path , future in futures :
117+ try :
118+ future .result ()
119+ successful_files .append (file_path )
120+ except Exception as e :
121+ failed_files .append (file_path )
122+ logger .error (f"Task failed for { file_path } : { str (e )} " )
123+
124+ # Log summary
125+ if successful_files :
126+ logger .info (f"\n Synced { len (successful_files )} files" )
127+ if failed_files :
128+ logger .error (f"Failed to sync { len (failed_files )} files" )
129+
130+ return successful_files
0 commit comments