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

patch(js): utf-8 encode multipart parts #1487

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "langsmith",
"version": "0.3.4",
"version": "0.3.5",
"description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.",
"packageManager": "yarn@1.22.19",
"files": [
Expand Down
30 changes: 15 additions & 15 deletions js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
import { raiseForStatus } from "./utils/error.js";
import { _getFetchImplementation } from "./singletons/fetch.js";

import { stringify as stringifyForTracing } from "./utils/fast-safe-stringify/index.js";
import { serialize as serializePayloadForTracing } from "./utils/fast-safe-stringify/index.js";

export interface ClientConfig {
apiUrl?: string;
Expand Down Expand Up @@ -405,7 +405,7 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
itemPromiseResolve = resolve;
});
const size = stringifyForTracing(item.item).length;
const size = serializePayloadForTracing(item.item).length;
this.items.push({
action: item.action,
payload: item.item,
Expand Down Expand Up @@ -439,7 +439,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 442 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 @@ -878,7 +878,7 @@
if (this._serverInfo === undefined) {
try {
this._serverInfo = await this._getServerInfo();
} catch (e) {

Check warning on line 881 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 @@ -943,7 +943,7 @@
{
method: "POST",
headers,
body: stringifyForTracing(mergedRunCreateParam),
body: serializePayloadForTracing(mergedRunCreateParam),
signal: AbortSignal.timeout(this.timeout_ms),
...this.fetchOptions,
}
Expand Down Expand Up @@ -1020,11 +1020,11 @@
}
}
if (batchChunks.post.length > 0 || batchChunks.patch.length > 0) {
await this._postBatchIngestRuns(stringifyForTracing(batchChunks));
await this._postBatchIngestRuns(serializePayloadForTracing(batchChunks));
}
}

private async _postBatchIngestRuns(body: string) {
private async _postBatchIngestRuns(body: Uint8Array) {
const headers = {
...this.headers,
"Content-Type": "application/json",
Expand Down Expand Up @@ -1143,7 +1143,7 @@
originalPayload;
const fields = { inputs, outputs, events };
// encode the main run payload
const stringifiedPayload = stringifyForTracing(payload);
const stringifiedPayload = serializePayloadForTracing(payload);
accumulatedParts.push({
name: `${method}.${payload.id}`,
payload: new Blob([stringifiedPayload], {
Expand All @@ -1155,7 +1155,7 @@
if (value === undefined) {
continue;
}
const stringifiedValue = stringifyForTracing(value);
const stringifiedValue = serializePayloadForTracing(value);
accumulatedParts.push({
name: `${method}.${payload.id}.${key}`,
payload: new Blob([stringifiedValue], {
Expand Down Expand Up @@ -1301,7 +1301,7 @@
{
method: "PATCH",
headers,
body: stringifyForTracing(run),
body: serializePayloadForTracing(run),
signal: AbortSignal.timeout(this.timeout_ms),
...this.fetchOptions,
}
Expand Down Expand Up @@ -1613,7 +1613,7 @@
treeFilter?: string;
isRoot?: boolean;
dataSourceType?: string;
}): Promise<any> {

Check warning on line 1616 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 @@ -1901,7 +1901,7 @@
`Failed to list shared examples: ${response.status} ${response.statusText}`
);
}
return result.map((example: any) => ({

Check warning on line 1904 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 @@ -2031,7 +2031,7 @@
}
// projectId querying
return true;
} catch (e) {

Check warning on line 2034 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 @@ -3521,7 +3521,7 @@
async _logEvaluationFeedback(
evaluatorResponse: EvaluationResult | EvaluationResults,
run?: Run,
sourceInfo?: { [key: string]: any }

Check warning on line 3524 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 @@ -3560,7 +3560,7 @@
public async logEvaluationFeedback(
evaluatorResponse: EvaluationResult | EvaluationResults,
run?: Run,
sourceInfo?: { [key: string]: any }

Check warning on line 3563 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 @@ -4056,7 +4056,7 @@

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

Check warning on line 4059 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 @@ -4124,15 +4124,15 @@
};

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

// Add inputs
if (example.inputs) {
const stringifiedInputs = stringifyForTracing(example.inputs);
const stringifiedInputs = serializePayloadForTracing(example.inputs);
const inputsBlob = new Blob([stringifiedInputs], {
type: "application/json",
});
Expand All @@ -4141,7 +4141,7 @@

// Add outputs if present
if (example.outputs) {
const stringifiedOutputs = stringifyForTracing(example.outputs);
const stringifiedOutputs = serializePayloadForTracing(example.outputs);
const outputsBlob = new Blob([stringifiedOutputs], {
type: "application/json",
});
Expand All @@ -4168,7 +4168,7 @@
}

if (example.attachments_operations) {
const stringifiedAttachmentsOperations = stringifyForTracing(
const stringifiedAttachmentsOperations = serializePayloadForTracing(
example.attachments_operations
);
const attachmentsOperationsBlob = new Blob(
Expand Down Expand Up @@ -4224,22 +4224,22 @@
};

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

// Add inputs
const stringifiedInputs = stringifyForTracing(example.inputs);
const stringifiedInputs = serializePayloadForTracing(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 stringifiedOutputs = serializePayloadForTracing(example.outputs);
const outputsBlob = new Blob([stringifiedOutputs], {
type: "application/json",
});
Expand Down Expand Up @@ -4288,7 +4288,7 @@
isPublic?: boolean;
isArchived?: boolean;
}
): Promise<Record<string, any>> {

Check warning on line 4291 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 @@ -4299,7 +4299,7 @@
throw await this._ownerConflictError("update a prompt", owner);
}

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

Check warning on line 4302 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
2 changes: 1 addition & 1 deletion js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export { RunTree, type RunTreeConfig } from "./run_trees.js";
export { overrideFetchImplementation } from "./singletons/fetch.js";

// Update using yarn bump-version
export const __version__ = "0.3.4";
export const __version__ = "0.3.5";
17 changes: 11 additions & 6 deletions js/src/tests/batch_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import { convertToDottedOrderFormat } from "../run_trees.js";
import { _getFetchImplementation } from "../singletons/fetch.js";
import { RunCreate } from "../schemas.js";

const parseMockRequestBody = async (body: string | ArrayBuffer) => {
const parseMockRequestBody = async (body: string | ArrayBuffer | Uint8Array) => {
if (typeof body === "string") {
return JSON.parse(body);
}

// Typing is missing
const rawMultipart = new TextDecoder().decode(body);

if (rawMultipart.trim().startsWith('{')) {
return JSON.parse(rawMultipart);
}
// Parse the multipart form data boundary from the raw text
const boundary = rawMultipart.split("\r\n")[0].trim();
// Split the multipart body into individual parts
Expand Down Expand Up @@ -143,7 +148,7 @@ describe.each(ENDPOINT_TYPES)(
_getFetchImplementation(),
expectedTraceURL,
expect.objectContaining({
body: expect.any(endpointType === "batch" ? String : ArrayBuffer),
body: expect.any(endpointType === "batch" ? Uint8Array : ArrayBuffer),
})
);
});
Expand Down Expand Up @@ -257,7 +262,7 @@ describe.each(ENDPOINT_TYPES)(
_getFetchImplementation(),
expectedTraceURL,
expect.objectContaining({
body: expect.any(endpointType === "batch" ? String : ArrayBuffer),
body: expect.any(endpointType === "batch" ? Uint8Array : ArrayBuffer),
})
);
});
Expand Down Expand Up @@ -338,7 +343,7 @@ describe.each(ENDPOINT_TYPES)(
_getFetchImplementation(),
expectedTraceURL,
expect.objectContaining({
body: expect.any(endpointType === "batch" ? String : ArrayBuffer),
body: expect.any(endpointType === "batch" ? Uint8Array : ArrayBuffer),
})
);
});
Expand Down Expand Up @@ -935,7 +940,7 @@ describe.each(ENDPOINT_TYPES)(
_getFetchImplementation(),
"https://api.smith.langchain.com/runs/batch",
expect.objectContaining({
body: expect.any(String),
body: expect.any(Uint8Array),
})
);
});
Expand Down Expand Up @@ -1027,7 +1032,7 @@ describe.each(ENDPOINT_TYPES)(
_getFetchImplementation(),
expectedTraceURL,
expect.objectContaining({
body: expect.any(endpointType === "batch" ? String : ArrayBuffer),
body: expect.any(endpointType === "batch" ? Uint8Array : ArrayBuffer),
})
);
});
Expand Down
19 changes: 14 additions & 5 deletions js/src/tests/utils/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,29 @@ export function getAssumedTreeFromCalls(calls: unknown[][]) {
{ method: string; body: string }
];
const req = `${fetchArgs.method} ${new URL(url as string).pathname}`;
const body: Run = JSON.parse(fetchArgs.body);

let body: Run;
if (typeof fetchArgs.body === "string") {
body = JSON.parse(fetchArgs.body);
} else {
const decoded = new TextDecoder().decode(fetchArgs.body);

if (decoded.trim().startsWith("{")) {
body = JSON.parse(decoded);
}
}

if (req === "POST /runs") {
const id = body.id;
const id = body!.id;
upsertId(id);
nodeMap[id] = { ...nodeMap[id], ...body };
nodeMap[id] = { ...nodeMap[id], ...body! };
if (nodeMap[id].parent_run_id) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
edges.push([nodeMap[id].parent_run_id!, nodeMap[id].id]);
}
} else if (req.startsWith("PATCH /runs/")) {
const id = req.substring("PATCH /runs/".length);
upsertId(id);
nodeMap[id] = { ...nodeMap[id], ...body };
nodeMap[id] = { ...nodeMap[id], ...body! };
}
}

Expand Down
21 changes: 14 additions & 7 deletions js/src/utils/fast-safe-stringify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@ var CIRCULAR_REPLACE_NODE = { result: "[Circular]" };
var arr = [];
var replacerStack = [];

const encoder = new TextEncoder();

function defaultOptions() {
return {
depthLimit: Number.MAX_SAFE_INTEGER,
edgesLimit: Number.MAX_SAFE_INTEGER,
};
}

function encodeString(str: string): Uint8Array {
return encoder.encode(str);
}

// Regular stringify
export function stringify(obj, replacer?, spacer?, options?) {
export function serialize(obj, replacer?, spacer?, options?) {
try {
return JSON.stringify(obj, replacer, spacer);
const str = JSON.stringify(obj, replacer, spacer);
return encodeString(str);
} catch (e: any) {
// Fall back to more complex stringify if circular reference
if (!e.message?.includes("Converting circular structure to JSON")) {
console.warn("[WARNING]: LangSmith received unserializable value.");
return "[Unserializable]";
return encodeString("[Unserializable]");
}
console.warn(
"[WARNING]: LangSmith received circular JSON. This will decrease tracer performance."
Expand All @@ -31,28 +38,28 @@ export function stringify(obj, replacer?, spacer?, options?) {
}

decirc(obj, "", 0, [], undefined, 0, options);
var res;
let res: string;
try {
if (replacerStack.length === 0) {
res = JSON.stringify(obj, replacer, spacer);
} else {
res = JSON.stringify(obj, replaceGetterValues(replacer), spacer);
}
} catch (_) {
return JSON.stringify(
return encodeString(
"[unable to serialize, circular reference is too complex to analyze]"
);
} finally {
while (arr.length !== 0) {
var part = arr.pop();
const part = arr.pop();
if (part.length === 4) {
Object.defineProperty(part[0], part[1], part[3]);
} else {
part[0][part[1]] = part[2];
}
}
}
return res;
return encodeString(res);
}
}

Expand Down
Loading