Skip to content

Commit 881f6e2

Browse files
committed
add environment and path filter to pull operation
1 parent f72fda0 commit 881f6e2

File tree

3 files changed

+44
-9
lines changed

3 files changed

+44
-9
lines changed

src/humanloop/client.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def __init__(
100100
opentelemetry_tracer_provider: Optional[TracerProvider] = None,
101101
opentelemetry_tracer: Optional[Tracer] = None,
102102
use_local_files: bool = False,
103+
files_directory: str = "humanloop",
103104
):
104105
"""
105106
Extends the base client with custom evaluation utilities and
@@ -120,7 +121,7 @@ def __init__(
120121
)
121122

122123
self.use_local_files = use_local_files
123-
self.sync_client = SyncClient(client=self)
124+
self._sync_client = SyncClient(client=self, base_dir=files_directory)
124125
eval_client = ExtendedEvalsClient(client_wrapper=self._client_wrapper)
125126
eval_client.client = self
126127
self.evaluations = eval_client
@@ -359,20 +360,26 @@ def agent():
359360
attributes=attributes,
360361
)
361362

362-
def pull(self) -> List[str]:
363+
def pull(self,
364+
environment: str | None = None,
365+
path: str | None = None
366+
) -> List[str]:
363367
"""Pull prompt and agent files from Humanloop to local filesystem.
364368
365369
This method will:
366370
1. Fetch all prompt and agent files from your Humanloop workspace
367-
2. Save them to the local filesystem in a 'humanloop/' directory
371+
2. Save them to the local filesystem using the client's files_directory (set during initialization)
368372
3. Maintain the same directory structure as in Humanloop
369373
4. Add appropriate file extensions (.prompt or .agent)
370374
375+
By default, the operation will overwrite existing files with the latest version from Humanlooop
376+
but will not delete local files that don't exist in the remote workspace.
377+
371378
Currently only supports syncing prompt and agent files. Other file types will be skipped.
372379
373380
The files will be saved with the following structure:
374381
```
375-
humanloop/
382+
{files_directory}/
376383
├── prompts/
377384
│ ├── my_prompt.prompt
378385
│ └── nested/
@@ -381,11 +388,16 @@ def pull(self) -> List[str]:
381388
└── my_agent.agent
382389
```
383390
384-
:return: List of successfully processed file paths
391+
:param environment: The environment to pull the files from.
392+
:param path: The path to the files to pull on the Humanloop workspace. Can be a directory or a specific file.
393+
:return: List of successfully processed file paths.
385394
"""
386-
return self.sync_client.pull()
387-
395+
return self._sync_client.pull(
396+
environment=environment,
397+
path=path
398+
)
388399

400+
389401
class AsyncHumanloop(AsyncBaseHumanloop):
390402
"""
391403
See docstring of AsyncBaseHumanloop.

src/humanloop/sync/sync_client.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,26 @@ def _save_serialized_file(self, serialized_content: str, file_path: str, file_ty
6565
logger.error(f"Failed to sync {file_type} {file_path}: {str(e)}")
6666
raise
6767

68-
def pull(self) -> List[str]:
68+
def pull(self,
69+
environment: str | None = None,
70+
directory: str | None = None,
71+
path: str | None = None,
72+
) -> List[str]:
6973
"""Sync prompt and agent files from Humanloop to local filesystem.
7074
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).
77+
If `environment` is provided, the files will be pulled from that environment.
78+
79+
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.
83+
7184
Returns:
7285
List of successfully processed file paths
7386
"""
87+
7488
successful_files = []
7589
failed_files = []
7690
page = 1
@@ -80,7 +94,8 @@ def pull(self) -> List[str]:
8094
response = self.client.files.list_files(
8195
type=["prompt", "agent"],
8296
page=page,
83-
include_content=True
97+
include_content=True,
98+
environment=environment
8499
)
85100

86101
if len(response.records) == 0:
@@ -93,6 +108,10 @@ def pull(self) -> List[str]:
93108
logger.warning(f"Skipping unsupported file type: {file.type}")
94109
continue
95110

111+
if not file.path.startswith(path):
112+
# Filter by path
113+
continue
114+
96115
# Skip if no content
97116
if not getattr(file, "content", None):
98117
logger.warning(f"No content found for {file.type} {getattr(file, 'id', '<unknown>')}")

tests/sync/test_sync.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def test_overload_log_with_local_files(humanloop_client: Humanloop, test_file_st
140140
1. Create files in remote (via test_file_structure fixture)
141141
2. Pull files locally
142142
3. Test logging using the pulled files
143+
144+
:param humanloop_client: The Humanloop client with local files enabled
145+
:param test_file_structure: List of test files created in remote
146+
:param cleanup_local_files: Fixture to clean up local files after test
143147
"""
144148
# First pull the files locally
145149
humanloop_client.pull()

0 commit comments

Comments
 (0)