Skip to content

Commit 39662df

Browse files
committed
refactor overload, moving functionality to sync client + implement LRU cache for reading local files
1 parent 816eaff commit 39662df

File tree

3 files changed

+72
-36
lines changed

3 files changed

+72
-36
lines changed

src/humanloop/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,12 @@ def __init__(
133133
self.prompts = overload_call(client=self.prompts)
134134
self.prompts = overload_with_local_files(
135135
client=self.prompts,
136+
sync_client=self._sync_client,
136137
use_local_files=self.use_local_files
137138
)
138139
self.agents = overload_with_local_files(
139140
client=self.agents,
141+
sync_client=self._sync_client,
140142
use_local_files=self.use_local_files
141143
)
142144
self.flows = overload_log(client=self.flows)

src/humanloop/overload.py

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from humanloop.prompts.client import PromptsClient
1616
from humanloop.agents.client import AgentsClient
1717
from humanloop.tools.client import ToolsClient
18+
from humanloop.sync.sync_client import SyncClient
1819
from humanloop.types import FileType
1920
from humanloop.types.create_evaluator_log_response import CreateEvaluatorLogResponse
2021
from humanloop.types.create_flow_log_response import CreateFlowLogResponse
@@ -135,37 +136,9 @@ def _get_file_type_from_client(client: Union[PromptsClient, AgentsClient]) -> Fi
135136
else:
136137
raise ValueError(f"Unsupported client type: {type(client)}")
137138

138-
def _handle_local_file(path: str, file_type: FileType) -> Optional[str]:
139-
"""Handle reading from a local file if it exists.
140-
141-
Args:
142-
path: The path to the file
143-
file_type: The type of file ("prompt" or "agent")
144-
145-
Returns:
146-
The file content if found, None otherwise
147-
"""
148-
try:
149-
# Construct path to local file
150-
local_path = Path("humanloop") / path # FLAG: ensure that when passing the path back to remote, it's using forward slashes
151-
# Add appropriate extension
152-
local_path = local_path.parent / f"{local_path.stem}.{file_type}"
153-
154-
if local_path.exists():
155-
# Read the file content
156-
with open(local_path) as f:
157-
file_content = f.read()
158-
logger.debug(f"Using local file content from {local_path}")
159-
return file_content
160-
else:
161-
logger.warning(f"Local file not found: {local_path}, falling back to API")
162-
return None
163-
except Exception as e:
164-
logger.error(f"Error reading local file: {e}, falling back to API")
165-
return None
166-
167139
def overload_with_local_files(
168140
client: Union[PromptsClient, AgentsClient],
141+
sync_client: SyncClient,
169142
use_local_files: bool,
170143
) -> Union[PromptsClient, AgentsClient]:
171144
"""Overload call and log methods to handle local files when use_local_files is True.
@@ -181,9 +154,11 @@ def overload_with_local_files(
181154
def _overload(self, function_name: str, **kwargs) -> PromptCallResponse:
182155
# Handle local files if enabled
183156
if use_local_files and "path" in kwargs:
184-
file_content = _handle_local_file(kwargs["path"], file_type)
157+
# Normalize the path and get file content
158+
normalized_path = sync_client._normalize_path(kwargs["path"])
159+
file_content = sync_client.get_file_content(normalized_path, file_type)
185160
if file_content is not None:
186-
kwargs[file_type] = file_content
161+
kwargs[file_type] = file_content
187162

188163
try:
189164
if function_name == "call":

src/humanloop/sync/sync_client.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import logging
44
from pathlib import Path
55
import concurrent.futures
6-
from typing import List, TYPE_CHECKING, Union, cast, Optional
6+
from typing import List, TYPE_CHECKING, Union, cast, Optional, Dict
7+
from functools import lru_cache
78

89
from humanloop.types import FileType, PromptResponse, AgentResponse, ToolResponse, DatasetResponse, EvaluatorResponse, FlowResponse
910
from humanloop.core.api_error import ApiError
@@ -21,24 +22,78 @@
2122
logger.addHandler(console_handler)
2223

2324
class SyncClient:
24-
"""Client for managing synchronization between local filesystem and Humanloop."""
25+
"""Client for managing synchronization between local filesystem and Humanloop.
26+
27+
This client provides file synchronization between Humanloop and the local filesystem,
28+
with built-in caching for improved performance. The cache uses Python's LRU (Least
29+
Recently Used) cache to automatically manage memory usage by removing least recently
30+
accessed files when the cache is full.
31+
32+
The cache is automatically updated when files are pulled or saved, and can be
33+
manually cleared using the clear_cache() method.
34+
"""
2535

2636
def __init__(
2737
self,
2838
client: "BaseHumanloop",
2939
base_dir: str = "humanloop",
30-
max_workers: Optional[int] = None
40+
cache_size: int = 100
3141
):
3242
"""
3343
Parameters
3444
----------
3545
client: Humanloop client instance
3646
base_dir: Base directory for synced files (default: "humanloop")
37-
max_workers: Maximum number of worker threads (default: CPU count * 2)
47+
cache_size: Maximum number of files to cache (default: 100)
3848
"""
3949
self.client = client
4050
self.base_dir = Path(base_dir)
41-
self.max_workers = max_workers or multiprocessing.cpu_count() * 2
51+
self._cache_size = cache_size
52+
# Create a new cached version of get_file_content with the specified cache size
53+
self.get_file_content = lru_cache(maxsize=cache_size)(self._get_file_content_impl)
54+
55+
def _get_file_content_impl(self, path: str, file_type: FileType) -> Optional[str]:
56+
"""Implementation of get_file_content without the cache decorator.
57+
58+
This is the actual implementation that gets wrapped by lru_cache.
59+
"""
60+
try:
61+
# Construct path to local file
62+
local_path = self.base_dir / path
63+
# Add appropriate extension
64+
local_path = local_path.parent / f"{local_path.stem}.{file_type}"
65+
66+
if local_path.exists():
67+
# Read the file content
68+
with open(local_path) as f:
69+
file_content = f.read()
70+
logger.debug(f"Using local file content from {local_path}")
71+
return file_content
72+
else:
73+
logger.warning(f"Local file not found: {local_path}, falling back to API")
74+
return None
75+
except Exception as e:
76+
logger.error(f"Error reading local file: {e}, falling back to API")
77+
return None
78+
79+
def get_file_content(self, path: str, file_type: FileType) -> Optional[str]:
80+
"""Get the content of a file from cache or filesystem.
81+
82+
This method uses an LRU cache to store file contents. When the cache is full,
83+
the least recently accessed files are automatically removed to make space.
84+
85+
Args:
86+
path: The normalized path to the file (without extension)
87+
file_type: The type of file (prompt or agent)
88+
89+
Returns:
90+
The file content if found, None otherwise
91+
"""
92+
return self._get_file_content_impl(path, file_type)
93+
94+
def clear_cache(self) -> None:
95+
"""Clear the LRU cache."""
96+
self.get_file_content.cache_clear()
4297

4398
def _normalize_path(self, path: str) -> str:
4499
"""Normalize the path by:
@@ -103,6 +158,10 @@ def _save_serialized_file(self, serialized_content: str, file_path: str, file_ty
103158
# Write content to file
104159
with open(new_path, "w") as f:
105160
f.write(serialized_content)
161+
162+
# Clear the cache for this file to ensure we get fresh content next time
163+
self.clear_cache()
164+
106165
logger.info(f"Syncing {file_type} {file_path}")
107166
except Exception as e:
108167
logger.error(f"Failed to sync {file_type} {file_path}: {str(e)}")

0 commit comments

Comments
 (0)