Skip to content

Commit b690578

Browse files
committed
refactor(sync): align FileSyncer with Python SDK and improve TypeScript idioms
- Rename SyncClient to FileSyncer and update client options to match Python SDK - Add useLocalFiles, localFilesDirectory, and cacheSize options - Update pull method to return [successful, failed] arrays - Improve TypeScript terminology in docstrings (array vs tuple/list) - Fix type errors and error handling Makes the codebase more consistent with Python SDK while improving TypeScript idioms and type safety.
1 parent 5436ac2 commit b690578

2 files changed

Lines changed: 88 additions & 39 deletions

File tree

src/humanloop.client.ts

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
import { HumanloopSpanExporter } from "./otel/exporter";
3030
import { HumanloopSpanProcessor } from "./otel/processor";
3131
import { overloadCall, overloadLog } from "./overload";
32-
import { SyncClient, FileSyncerOptions } from "./sync";
32+
import { FileSyncerOptions, SyncClient } from "./sync";
33+
import Logger from "./utils/Logger";
3334
import { SDK_VERSION } from "./version";
3435

3536
const RED = "\x1b[91m";
@@ -200,6 +201,39 @@ class HumanloopTracerSingleton {
200201
}
201202
}
202203

204+
export interface HumanloopClientOptions extends BaseHumanloopClient.Options {
205+
/**
206+
* Whether to use local files for prompts and agents
207+
*/
208+
useLocalFiles?: boolean;
209+
210+
/**
211+
* Base directory where local prompt and agent files are stored (default: "humanloop").
212+
* This is relative to the current working directory. For example:
213+
* - "humanloop" will look for files in "./humanloop/"
214+
* - "data/humanloop" will look for files in "./data/humanloop/"
215+
* When using paths in the API, they must be relative to this directory. For example,
216+
* if localFilesDirectory="humanloop" and you have a file at "humanloop/samples/test.prompt",
217+
* you would reference it as "samples/test" in your code.
218+
*/
219+
localFilesDirectory?: string;
220+
221+
/**
222+
* Maximum number of files to cache when useLocalFiles is true (default: DEFAULT_CACHE_SIZE).
223+
* This parameter has no effect if useLocalFiles is false.
224+
*/
225+
cacheSize?: number;
226+
227+
/**
228+
* LLM provider modules to instrument. Allows the prompt decorator to spy on provider calls and log them to Humanloop
229+
*/
230+
instrumentProviders?: {
231+
OpenAI?: any;
232+
Anthropic?: any;
233+
CohereAI?: any;
234+
};
235+
}
236+
203237
export class HumanloopClient extends BaseHumanloopClient {
204238
protected readonly _evaluations: ExtendedEvaluations;
205239
protected readonly _prompts_overloaded: Prompts;
@@ -212,6 +246,7 @@ export class HumanloopClient extends BaseHumanloopClient {
212246
CohereAI?: any;
213247
};
214248
protected readonly _syncClient: SyncClient;
249+
protected readonly useLocalFiles: boolean;
215250

216251
protected get opentelemetryTracer(): Tracer {
217252
return HumanloopTracerSingleton.getInstance({
@@ -245,21 +280,25 @@ export class HumanloopClient extends BaseHumanloopClient {
245280
* const anthropic = new Anthropic({apiKey: process.env.ANTHROPIC_KEY});
246281
* ```
247282
*/
248-
constructor(
249-
_options: BaseHumanloopClient.Options & {
250-
instrumentProviders?: {
251-
OpenAI?: any;
252-
Anthropic?: any;
253-
CohereAI?: any;
254-
};
255-
sync?: FileSyncerOptions;
256-
},
257-
) {
258-
super(_options);
259-
260-
this._syncClient = new SyncClient(this, _options.sync);
261-
262-
this.instrumentProviders = _options.instrumentProviders || {};
283+
constructor(options: HumanloopClientOptions = {}) {
284+
super(options);
285+
286+
this.useLocalFiles = options.useLocalFiles || false;
287+
288+
// Warn user if cacheSize is non-default but useLocalFiles is false
289+
if (!this.useLocalFiles && options.cacheSize !== undefined) {
290+
Logger.warn(
291+
`The specified cacheSize=${options.cacheSize} will have no effect because useLocalFiles=false. ` +
292+
`File caching is only active when local files are enabled.`,
293+
);
294+
}
295+
296+
this._syncClient = new SyncClient(this, {
297+
baseDir: options.localFilesDirectory || "humanloop",
298+
cacheSize: options.cacheSize,
299+
});
300+
301+
this.instrumentProviders = options.instrumentProviders || {};
263302

264303
this._prompts_overloaded = overloadLog(super.prompts);
265304
this._prompts_overloaded = overloadCall(this._prompts_overloaded);
@@ -270,7 +309,7 @@ export class HumanloopClient extends BaseHumanloopClient {
270309

271310
this._evaluators_overloaded = overloadLog(super.evaluators);
272311

273-
this._evaluations = new ExtendedEvaluations(_options, this);
312+
this._evaluations = new ExtendedEvaluations(options, this);
274313

275314
// Initialize the tracer singleton
276315
HumanloopTracerSingleton.getInstance({
@@ -364,14 +403,14 @@ ${RESET}`,
364403
* temperature: 0.5,
365404
* });
366405
* const openaiContent = openaiResponse.choices[0].message.content;
367-
*
406+
368407
* const anthropicClient = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
369408
* const anthropicResponse = await anthropicClient.messages.create({
370409
* model: "claude-3-5-sonnet-20240620",
371410
* temperature: 0.5,
372411
* });
373412
* const anthropicContent = anthropicResponse.content;
374-
*
413+
375414
* return { openaiContent, anthropicContent };
376415
* }
377416
* });
@@ -570,40 +609,50 @@ ${RESET}`,
570609
*
571610
* This method will:
572611
* 1. Fetch Prompt and Agent files from your Humanloop workspace
573-
* 2. Save them to the local filesystem using the client's files_directory (set during initialization)
612+
* 2. Save them to your local filesystem (directory specified by `localFilesDirectory`, default: "humanloop")
574613
* 3. Maintain the same directory structure as in Humanloop
575-
* 4. Add appropriate file extensions (.prompt or .agent)
614+
* 4. Add appropriate file extensions (`.prompt` or `.agent`)
576615
*
577616
* The path parameter can be used in two ways:
578617
* - If it points to a specific file (e.g. "path/to/file.prompt" or "path/to/file.agent"), only that file will be pulled
579-
* - If it points to a directory (e.g. "path/to/directory"), all Prompt and Agent files in that directory will be pulled
618+
* - If it points to a directory (e.g. "path/to/directory"), all Prompt and Agent files in that directory and its subdirectories will be pulled
580619
* - If no path is provided, all Prompt and Agent files will be pulled
581620
*
582621
* The operation will overwrite existing files with the latest version from Humanloop
583622
* but will not delete local files that don't exist in the remote workspace.
584623
*
585-
* Currently only supports syncing prompt and agent files. Other file types will be skipped.
624+
* Currently only supports syncing Prompt and Agent files. Other file types will be skipped.
586625
*
587-
* The files will be saved with the following structure:
626+
* For example, with the default `localFilesDirectory="humanloop"`, files will be saved as:
588627
* ```
589-
* {files_directory}/
590-
* ├── prompts/
591-
* │ ├── my_prompt.prompt
592-
* │ └── nested/
593-
* │ └── another_prompt.prompt
594-
* └── agents/
595-
* └── my_agent.agent
628+
* ./humanloop/
629+
* ├── my_project/
630+
* │ ├── prompts/
631+
* │ │ ├── my_prompt.prompt
632+
* │ │ └── nested/
633+
* │ │ └── another_prompt.prompt
634+
* │ └── agents/
635+
* │ └── my_agent.agent
636+
* └── another_project/
637+
* └── prompts/
638+
* └── other_prompt.prompt
596639
* ```
597640
*
641+
* If you specify `localFilesDirectory="data/humanloop"`, files will be saved in ./data/humanloop/ instead.
642+
*
598643
* @param path - Optional path to either a specific file (e.g. "path/to/file.prompt") or a directory (e.g. "path/to/directory").
599644
* If not provided, all Prompt and Agent files will be pulled.
600645
* @param environment - The environment to pull the files from.
601-
* @returns List of successfully processed file paths.
646+
* @returns An array containing two string arrays:
647+
* - First array contains paths of successfully synced files
648+
* - Second array contains paths of files that failed to sync (due to API errors, missing content,
649+
* or filesystem issues)
650+
* @throws HumanloopRuntimeError If there's an error communicating with the API
602651
*/
603652
public async pull(
604653
path?: string,
605654
environment?: string,
606-
): Promise<string[]> {
655+
): Promise<[string[], string[]]> {
607656
return this._syncClient.pull(path, environment);
608657
}
609658

src/sync/FileSyncer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,9 @@ export default class FileSyncer {
250250
/**
251251
* Sync Prompt and Agent files from Humanloop to local filesystem.
252252
*
253-
* @returns Tuple of two lists:
254-
* - First list contains paths of successfully pulled files
255-
* - Second list contains paths of files that failed to pull.
253+
* @returns An array containing two string arrays:
254+
* - First array contains paths of successfully pulled files
255+
* - Second array contains paths of files that failed to pull.
256256
* Failures can occur due to missing content in the response or errors during local file writing.
257257
* @throws HumanloopRuntimeError If there's an error communicating with the API
258258
*/
@@ -357,9 +357,9 @@ export default class FileSyncer {
357357
*
358358
* Paths should not contain leading or trailing slashes
359359
* @param environment The environment to pull from
360-
* @returns Tuple of two lists:
361-
* - First list contains paths of successfully pulled files
362-
* - Second list contains paths of files that failed to pull (e.g. failed to write to disk or missing raw content)
360+
* @returns An array containing two string arrays:
361+
* - First array contains paths of successfully pulled files
362+
* - Second array contains paths of files that failed to pull (e.g. failed to write to disk or missing raw content)
363363
* @throws HumanloopRuntimeError If there's an error communicating with the API
364364
*/
365365
public async pull(

0 commit comments

Comments
 (0)