Skip to content

Commit 8ac8a87

Browse files
committed
Create sync client + LRU cache + pull operation on extended client
1 parent 981f9d3 commit 8ac8a87

File tree

9 files changed

+490
-2
lines changed

9 files changed

+490
-2
lines changed

.fernignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ src/humanloop.client.ts
1010
src/overload.ts
1111
src/error.ts
1212
src/context.ts
13+
src/sync
14+
src/cache
1315

1416
# Tests
1517

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"version": "0.8.21",
44
"private": false,
55
"repository": "https://github.com/humanloop/humanloop-node",
6-
"main": "./index.js",
7-
"types": "./index.d.ts",
6+
"main": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
88
"scripts": {
99
"format": "prettier . --write --ignore-unknown",
1010
"build": "tsc",

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/humanloop.client.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
44
import { AnthropicInstrumentation } from "@traceloop/instrumentation-anthropic";
55
import { CohereInstrumentation } from "@traceloop/instrumentation-cohere";
66
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
7+
import { SyncClient } from "./sync";
78

89
import { HumanloopClient as BaseHumanloopClient } from "./Client";
910
import { ChatMessage } from "./api";
@@ -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({
@@ -254,6 +256,8 @@ export class HumanloopClient extends BaseHumanloopClient {
254256
) {
255257
super(_options);
256258

259+
this._syncClient = new SyncClient(this);
260+
257261
this.instrumentProviders = _options.instrumentProviders || {};
258262

259263
this._prompts_overloaded = overloadLog(super.prompts);
@@ -560,6 +564,45 @@ ${RESET}`,
560564
);
561565
}
562566

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

src/sync/MetadataHandler.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import fs from "fs";
2+
import path from "path";
3+
4+
/**
5+
* Interface for operation log entries
6+
*/
7+
interface OperationLog {
8+
operationType: string;
9+
path: string;
10+
environment?: string;
11+
successfulFiles?: string[];
12+
failedFiles?: string[];
13+
error?: string;
14+
startTime: number;
15+
endTime: number;
16+
duration: number;
17+
}
18+
19+
/**
20+
* Parameters for the logOperation method
21+
*/
22+
interface LogOperationParams {
23+
operationType: string;
24+
path: string;
25+
environment?: string;
26+
successfulFiles?: string[];
27+
failedFiles?: string[];
28+
error?: string;
29+
startTime: number;
30+
}
31+
32+
/**
33+
* Handler for managing metadata and operation logs
34+
*/
35+
export default class MetadataHandler {
36+
private baseDir: string;
37+
private metadataDir: string;
38+
private operationsLogPath: string;
39+
40+
constructor(baseDir: string) {
41+
this.baseDir = baseDir;
42+
this.metadataDir = path.join(baseDir, ".humanloop");
43+
this.operationsLogPath = path.join(this.metadataDir, "operations.log");
44+
45+
// Create metadata directory if it doesn't exist
46+
if (!fs.existsSync(this.metadataDir)) {
47+
fs.mkdirSync(this.metadataDir, { recursive: true });
48+
}
49+
}
50+
51+
/**
52+
* Log an operation to the operations log file
53+
*/
54+
public logOperation(params: LogOperationParams): void {
55+
const endTime = Date.now();
56+
const duration = endTime - params.startTime;
57+
58+
const logEntry: OperationLog = {
59+
...params,
60+
endTime,
61+
duration,
62+
};
63+
64+
try {
65+
// Create the log file if it doesn't exist
66+
if (!fs.existsSync(this.operationsLogPath)) {
67+
fs.writeFileSync(this.operationsLogPath, "");
68+
}
69+
70+
// Append the log entry to the file
71+
fs.appendFileSync(this.operationsLogPath, JSON.stringify(logEntry) + "\n");
72+
} catch (error) {
73+
console.error(`Error logging operation: ${error}`);
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)