Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upsert examples multipart in JS #1216

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
AnnotationQueue,
RunWithAnnotationQueueInfo,
Attachments,
ExampleUpsertWithAttachments,
UpsertExamplesResponse
} from "./schemas.js";
import {
convertLangChainMessageToExample,
Expand Down Expand Up @@ -420,7 +422,7 @@
// If there is an item on the queue we were unable to pop,
// just return it as a single batch.
if (popped.length === 0 && this.items.length > 0) {
const item = this.items.shift()!;

Check warning on line 425 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Forbidden non-null assertion
popped.push(item);
poppedSizeBytes += item.size;
this.sizeBytes -= item.size;
Expand Down Expand Up @@ -836,7 +838,7 @@
if (this._serverInfo === undefined) {
try {
this._serverInfo = await this._getServerInfo();
} catch (e) {

Check warning on line 841 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

'e' is defined but never used. Allowed unused args must match /^_/u
console.warn(
`[WARNING]: LangSmith failed to fetch info on supported operations. Falling back to batch operations and default limits.`
);
Expand Down Expand Up @@ -1536,7 +1538,7 @@
treeFilter?: string;
isRoot?: boolean;
dataSourceType?: string;
}): Promise<any> {

Check warning on line 1541 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
let projectIds_ = projectIds || [];
if (projectNames) {
projectIds_ = [
Expand Down Expand Up @@ -1824,7 +1826,7 @@
`Failed to list shared examples: ${response.status} ${response.statusText}`
);
}
return result.map((example: any) => ({

Check warning on line 1829 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
...example,
_hostUrl: this.getHostUrl(),
}));
Expand Down Expand Up @@ -1961,7 +1963,7 @@
}
// projectId querying
return true;
} catch (e) {

Check warning on line 1966 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

'e' is defined but never used. Allowed unused args must match /^_/u
return false;
}
}
Expand Down Expand Up @@ -3301,7 +3303,7 @@
async _logEvaluationFeedback(
evaluatorResponse: EvaluationResult | EvaluationResults,
run?: Run,
sourceInfo?: { [key: string]: any }

Check warning on line 3306 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
): Promise<[results: EvaluationResult[], feedbacks: Feedback[]]> {
const evalResults: Array<EvaluationResult> =
this._selectEvalResults(evaluatorResponse);
Expand Down Expand Up @@ -3340,7 +3342,7 @@
public async logEvaluationFeedback(
evaluatorResponse: EvaluationResult | EvaluationResults,
run?: Run,
sourceInfo?: { [key: string]: any }

Check warning on line 3345 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
): Promise<EvaluationResult[]> {
const [results] = await this._logEvaluationFeedback(
evaluatorResponse,
Expand Down Expand Up @@ -3790,7 +3792,7 @@

public async createCommit(
promptIdentifier: string,
object: any,

Check warning on line 3795 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
options?: {
parentCommitHash?: string;
}
Expand Down Expand Up @@ -3832,6 +3834,75 @@
);
}

/**
* Upsert examples with attachments using multipart form data.
* @param upserts List of ExampleUpsertWithAttachments objects to upsert
* @returns Promise with the upsert response
*/
public async upsertExamplesMultipart({
upserts = [],
}: {
upserts?: ExampleUpsertWithAttachments[];
}): Promise<UpsertExamplesResponse> {
const formData = new FormData();

for (const example of upserts) {
const exampleId = (example.id ?? uuid.v4()).toString();

// Prepare the main example body
const exampleBody = {
dataset_id: example.dataset_id,
created_at: example.created_at,
...(example.metadata && { metadata: example.metadata }),
...(example.split && { split: example.split }),
};

// Add main example data
const stringifiedExample = stringifyForTracing(exampleBody);
const exampleBlob = new Blob([stringifiedExample], {
type: "application/json"
});
formData.append(exampleId, exampleBlob);

// Add inputs
const stringifiedInputs = stringifyForTracing(example.inputs);
const inputsBlob = new Blob([stringifiedInputs], {
type: "application/json"
});
formData.append(`${exampleId}.inputs`, inputsBlob);

// Add outputs if present
if (example.outputs) {
const stringifiedOutputs = stringifyForTracing(example.outputs);
const outputsBlob = new Blob([stringifiedOutputs], {
type: "application/json"
});
formData.append(`${exampleId}.outputs`, outputsBlob);
}

// Add attachments if present
if (example.attachments) {
for (const [name, [mimeType, data]] of Object.entries(example.attachments)) {
const attachmentBlob = new Blob([data], { type: `${mimeType}; length=${data.byteLength}` });
formData.append(`${exampleId}.attachment.${name}`, attachmentBlob);
}
}
}

const response = await this.caller.call(
_getFetchImplementation(),
`${this.apiUrl}/v1/platform/examples/multipart`,
{
method: "POST",
headers: this.headers,
body: formData,
}
);
const result = await response.json();
return result;

}

public async updatePrompt(
promptIdentifier: string,
options?: {
Expand All @@ -3841,7 +3912,7 @@
isPublic?: boolean;
isArchived?: boolean;
}
): Promise<Record<string, any>> {

Check warning on line 3915 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
if (!(await this.promptExists(promptIdentifier))) {
throw new Error("Prompt does not exist, you must create it first.");
}
Expand All @@ -3852,7 +3923,7 @@
throw await this._ownerConflictError("update a prompt", owner);
}

const payload: Record<string, any> = {};

Check warning on line 3926 in js/src/client.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type

if (options?.description !== undefined)
payload.description = options.description;
Expand Down
9 changes: 9 additions & 0 deletions js/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,15 @@ export interface ExampleCreate extends BaseExample {
split?: string | string[];
}

export interface ExampleUpsertWithAttachments extends ExampleCreate {
attachments?: Attachments;
}

export interface UpsertExamplesResponse {
count: number;
example_ids: string[];
}

export interface Example extends BaseExample {
id: string;
created_at: string;
Expand Down
122 changes: 122 additions & 0 deletions js/src/tests/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { jest } from "@jest/globals";
import { v4 as uuidv4 } from "uuid";
import { Client } from "../client.js";
import {
getEnvironmentVariables,
Expand All @@ -10,6 +11,10 @@ import {
isVersionGreaterOrEqual,
parsePromptIdentifier,
} from "../utils/prompts.js";
import { ExampleUpsertWithAttachments } from "../schemas.js";
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";

describe("Client", () => {
describe("createLLMExample", () => {
Expand Down Expand Up @@ -249,4 +254,121 @@ describe("Client", () => {
});
});
});

describe("upsertExamplesMultipart", () => {
it("should upsert examples with attachments via multipart endpoint", async () => {
const datasetName = `__test_upsert_examples_multipart${uuidv4().slice(0, 4)}`;
// NEED TO FIX THIS AFTER ENDPOINT MAKES IT TO PROD
const client = new Client({
apiUrl: "https://dev.api.smith.langchain.com",
apiKey: "HARDCODE FOR TESTING"
});

// Clean up existing dataset if it exists
if (await client.hasDataset({ datasetName })) {
await client.deleteDataset({ datasetName });
}

// Create actual dataset
const dataset = await client.createDataset(
datasetName, {
description: "Test dataset for multipart example upload",
dataType: "kv"
}
);

const pathname = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"test_data",
"parrot-icon.png"
);
// Create test examples
const exampleId = uuidv4();
const example1: ExampleUpsertWithAttachments = {
id: exampleId,
dataset_id: dataset.id,
inputs: { text: "hello world" },
// check that passing no outputs works fine
attachments: {
test_file: ["image/png", fs.readFileSync(pathname)],
},
};

const example2: ExampleUpsertWithAttachments = {
dataset_id: dataset.id,
inputs: { text: "foo bar" },
outputs: { response: "baz" },
attachments: {
my_file: ["image/png", fs.readFileSync(pathname)],
},
};

// Test creating examples
const createdExamples = await client.upsertExamplesMultipart({
upserts: [
example1,
example2,
]});

expect(createdExamples.count).toBe(2);

const createdExample1 = await client.readExample(
createdExamples.example_ids[0]
);
expect(createdExample1.inputs["text"]).toBe("hello world");

const createdExample2 = await client.readExample(
createdExamples.example_ids[1]
);
expect(createdExample2.inputs["text"]).toBe("foo bar");
expect(createdExample2.outputs?.["response"]).toBe("baz");

// Test examples were sent to correct dataset
const allExamplesInDataset = [];
for await (const example of client.listExamples({
datasetId: dataset.id,
})) {
allExamplesInDataset.push(example);
}
expect(allExamplesInDataset.length).toBe(2);

// Test updating example
const example1Update: ExampleUpsertWithAttachments = {
id: exampleId,
dataset_id: dataset.id,
inputs: { text: "bar baz" },
outputs: { response: "foo" },
attachments: {
my_file: ["image/png", fs.readFileSync(pathname)],
},
};

const updatedExamples = await client.upsertExamplesMultipart({
upserts: [
example1Update,
]});
expect(updatedExamples.count).toBe(1);
expect(updatedExamples.example_ids[0]).toBe(exampleId);

const updatedExample = await client.readExample(updatedExamples.example_ids[0]);
expect(updatedExample.inputs["text"]).toBe("bar baz");
expect(updatedExample.outputs?.["response"]).toBe("foo");

// Test invalid example fails
const example3: ExampleUpsertWithAttachments = {
dataset_id: uuidv4(), // not a real dataset
inputs: { text: "foo bar" },
outputs: { response: "baz" },
attachments: {
my_file: ["image/png", fs.readFileSync(pathname)],
},
};

const errorResponse = await client.upsertExamplesMultipart({ upserts: [example3] });
expect(errorResponse).toHaveProperty("error");

// Clean up
await client.deleteDataset({ datasetName });
});
});
});
Loading