Skip to content

Commit 276cab8

Browse files
committed
Merge custom code from old p_sync/pull branch with latest autogenerated SDK
1 parent 626fd01 commit 276cab8

File tree

15 files changed

+2060
-13
lines changed

15 files changed

+2060
-13
lines changed

.fernignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ src/humanloop.client.ts
1010
src/overload.ts
1111
src/error.ts
1212
src/context.ts
13+
src/cli.ts
14+
src/cache
15+
src/sync
16+
src/utils
1317

1418
# Tests
1519

1620
# Modified due to issues with OTEL
1721
tests/unit/fetcher/stream-wrappers/webpack.test.ts
22+
tests/custom/
1823

1924
# CI Action
2025

src/cache/LRUCache.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* LRU Cache implementation
3+
*/
4+
export default class LRUCache<K, V> {
5+
private cache: Map<K, V>;
6+
private readonly maxSize: number;
7+
8+
constructor(maxSize: number) {
9+
this.cache = new Map<K, V>();
10+
this.maxSize = maxSize;
11+
}
12+
13+
get(key: K): V | undefined {
14+
if (!this.cache.has(key)) {
15+
return undefined;
16+
}
17+
18+
// Get the value
19+
const value = this.cache.get(key);
20+
21+
// Remove key and re-insert to mark as most recently used
22+
this.cache.delete(key);
23+
this.cache.set(key, value!);
24+
25+
return value;
26+
}
27+
28+
set(key: K, value: V): void {
29+
// If key already exists, refresh its position
30+
if (this.cache.has(key)) {
31+
this.cache.delete(key);
32+
}
33+
// If cache is full, remove the least recently used item (first item in the map)
34+
else if (this.cache.size >= this.maxSize) {
35+
const lruKey = this.cache.keys().next().value;
36+
if (lruKey) {
37+
this.cache.delete(lruKey);
38+
}
39+
}
40+
41+
// Add new key-value pair
42+
this.cache.set(key, value);
43+
}
44+
45+
clear(): void {
46+
this.cache.clear();
47+
}
48+
}

src/cache/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as LRUCache } from './LRUCache';

src/cli.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env node
2+
import * as dotenv from "dotenv";
3+
import { Command } from "commander";
4+
5+
import { HumanloopClient } from "./humanloop.client";
6+
import Logger from "./utils/Logger";
7+
8+
const { version } = require("../package.json");
9+
10+
// Load environment variables
11+
dotenv.config();
12+
13+
const program = new Command();
14+
program
15+
.name("humanloop")
16+
.description("Humanloop CLI for managing sync operations")
17+
.version(version);
18+
19+
// Common auth options
20+
const addAuthOptions = (command: Command) =>
21+
command
22+
.option("--api-key <apiKey>", "Humanloop API key")
23+
.option("--env-file <envFile>", "Path to .env file")
24+
.option("--base-url <baseUrl>", "Base URL for Humanloop API");
25+
26+
// Helper to get client
27+
function getClient(options: {
28+
envFile?: string;
29+
apiKey?: string;
30+
baseUrl?: string;
31+
baseDir?: string;
32+
}): HumanloopClient {
33+
if (options.envFile) dotenv.config({ path: options.envFile });
34+
const apiKey = options.apiKey || process.env.HUMANLOOP_API_KEY;
35+
if (!apiKey) {
36+
Logger.error(
37+
"No API key found. Set HUMANLOOP_API_KEY in .env file or use --api-key",
38+
);
39+
process.exit(1);
40+
}
41+
return new HumanloopClient({
42+
apiKey,
43+
baseUrl: options.baseUrl,
44+
sync: { baseDir: options.baseDir },
45+
});
46+
}
47+
48+
// Pull command
49+
addAuthOptions(
50+
program
51+
.command("pull")
52+
.description("Pull files from Humanloop to local filesystem")
53+
.option("-p, --path <path>", "Path to pull (file or directory)")
54+
.option("-e, --environment <env>", "Environment to pull from")
55+
.option("--base-dir <baseDir>", "Base directory for synced files", "humanloop"),
56+
).action(async (options) => {
57+
Logger.info("Pulling files from Humanloop...");
58+
// try {
59+
// Logger.info("Pulling files from Humanloop...");
60+
// const client = getClient(options);
61+
// const files = await client.pull(options.path, options.environment);
62+
// Logger.success(`Successfully synced ${files.length} files`);
63+
// } catch (error) {
64+
// Logger.error(`Error: ${error}`);
65+
// process.exit(1);
66+
// }
67+
});
68+
69+
program.parse(process.argv);

src/humanloop.client.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import { HumanloopSpanExporter } from "./otel/exporter";
3030
import { HumanloopSpanProcessor } from "./otel/processor";
3131
import { overloadCall, overloadLog } from "./overload";
32+
import { SyncClient, SyncClientOptions } from "./sync";
3233
import { SDK_VERSION } from "./version";
3334

3435
const RED = "\x1b[91m";
@@ -210,6 +211,7 @@ export class HumanloopClient extends BaseHumanloopClient {
210211
Anthropic?: any;
211212
CohereAI?: any;
212213
};
214+
protected readonly _syncClient: SyncClient;
213215

214216
protected get opentelemetryTracer(): Tracer {
215217
return HumanloopTracerSingleton.getInstance({
@@ -250,10 +252,13 @@ export class HumanloopClient extends BaseHumanloopClient {
250252
Anthropic?: any;
251253
CohereAI?: any;
252254
};
255+
sync?: SyncClientOptions;
253256
},
254257
) {
255258
super(_options);
256259

260+
this._syncClient = new SyncClient(this, _options.sync);
261+
257262
this.instrumentProviders = _options.instrumentProviders || {};
258263

259264
this._prompts_overloaded = overloadLog(super.prompts);
@@ -560,6 +565,48 @@ ${RESET}`,
560565
);
561566
}
562567

568+
/**
569+
* Pull Prompt and Agent files from Humanloop to local filesystem.
570+
*
571+
* This method will:
572+
* 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)
574+
* 3. Maintain the same directory structure as in Humanloop
575+
* 4. Add appropriate file extensions (.prompt or .agent)
576+
*
577+
* The path parameter can be used in two ways:
578+
* - 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
580+
* - If no path is provided, all Prompt and Agent files will be pulled
581+
*
582+
* The operation will overwrite existing files with the latest version from Humanloop
583+
* but will not delete local files that don't exist in the remote workspace.
584+
*
585+
* Currently only supports syncing prompt and agent files. Other file types will be skipped.
586+
*
587+
* The files will be saved with the following structure:
588+
* ```
589+
* {files_directory}/
590+
* ├── prompts/
591+
* │ ├── my_prompt.prompt
592+
* │ └── nested/
593+
* │ └── another_prompt.prompt
594+
* └── agents/
595+
* └── my_agent.agent
596+
* ```
597+
*
598+
* @param path - Optional path to either a specific file (e.g. "path/to/file.prompt") or a directory (e.g. "path/to/directory").
599+
* If not provided, all Prompt and Agent files will be pulled.
600+
* @param environment - The environment to pull the files from.
601+
* @returns List of successfully processed file paths.
602+
*/
603+
public async pull(
604+
path?: string,
605+
environment?: string,
606+
): Promise<string[]> {
607+
return this._syncClient.pull(path, environment);
608+
}
609+
563610
public get evaluations(): ExtendedEvaluations {
564611
return this._evaluations;
565612
}

0 commit comments

Comments
 (0)