diff --git a/js/package.json b/js/package.json index cb7212cd1..2f2579415 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "langsmith", - "version": "0.1.54", + "version": "0.1.55", "description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.", "packageManager": "yarn@1.22.19", "files": [ @@ -276,4 +276,4 @@ }, "./package.json": "./package.json" } -} \ No newline at end of file +} diff --git a/js/src/client.ts b/js/src/client.ts index a11ff507a..5a57c3651 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -56,9 +56,10 @@ import { parsePromptIdentifier, } from "./utils/prompts.js"; import { raiseForStatus } from "./utils/error.js"; -import { stringifyForTracing } from "./utils/serde.js"; import { _getFetchImplementation } from "./singletons/fetch.js"; +import { stringify as stringifyForTracing } from "./utils/fast-safe-stringify/index.js"; + export interface ClientConfig { apiUrl?: string; apiKey?: string; diff --git a/js/src/index.ts b/js/src/index.ts index 199c5bfdb..c08746049 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -14,4 +14,4 @@ export { RunTree, type RunTreeConfig } from "./run_trees.js"; export { overrideFetchImplementation } from "./singletons/fetch.js"; // Update using yarn bump-version -export const __version__ = "0.1.54"; +export const __version__ = "0.1.55"; diff --git a/js/src/tests/batch_client.test.ts b/js/src/tests/batch_client.test.ts index 7dec85149..fd73237cc 100644 --- a/js/src/tests/batch_client.test.ts +++ b/js/src/tests/batch_client.test.ts @@ -3,7 +3,6 @@ import { jest } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { Client } from "../client.js"; import { convertToDottedOrderFormat } from "../run_trees.js"; -import { CIRCULAR_VALUE_REPLACEMENT_STRING } from "../utils/serde.js"; import { _getFetchImplementation } from "../singletons/fetch.js"; describe("Batch client tracing", () => { @@ -568,12 +567,14 @@ describe("Batch client tracing", () => { inputs: { b: { a: { - result: CIRCULAR_VALUE_REPLACEMENT_STRING, + result: "[Circular]", }, }, }, outputs: { - result: CIRCULAR_VALUE_REPLACEMENT_STRING, + a: { + result: "[Circular]", + }, }, end_time: endTime, trace_id: runId, diff --git a/js/src/tests/traceable.test.ts b/js/src/tests/traceable.test.ts index 19cbf7f74..0686e1ec9 100644 --- a/js/src/tests/traceable.test.ts +++ b/js/src/tests/traceable.test.ts @@ -2,7 +2,6 @@ import { RunTree, RunTreeConfig } from "../run_trees.js"; import { ROOT, traceable, withRunTree } from "../traceable.js"; import { getAssumedTreeFromCalls } from "./utils/tree.js"; import { mockClient } from "./utils/mock_client.js"; -import { CIRCULAR_VALUE_REPLACEMENT_STRING } from "../utils/serde.js"; test("basic traceable implementation", async () => { const { client, callSpy } = mockClient(); @@ -78,13 +77,20 @@ test("trace circular input and output objects", async () => { a.b = b; b.a = a; const llm = traceable( - async function foo(_: any) { + async function foo(_: Record) { return a; }, { client, tracingEnabled: true } ); - await llm(a); + const input = { + a, + a2: a, + normalParam: { + test: true, + }, + }; + await llm(input); expect(getAssumedTreeFromCalls(callSpy.mock.calls)).toMatchObject({ nodes: ["foo:0"], @@ -92,14 +98,30 @@ test("trace circular input and output objects", async () => { data: { "foo:0": { inputs: { - b: { - a: { - result: CIRCULAR_VALUE_REPLACEMENT_STRING, + a: { + b: { + a: { + result: "[Circular]", + }, + }, + }, + a2: { + b: { + a: { + result: "[Circular]", + }, }, }, + normalParam: { + test: true, + }, }, outputs: { - result: CIRCULAR_VALUE_REPLACEMENT_STRING, + b: { + a: { + result: "[Circular]", + }, + }, }, }, }, diff --git a/js/src/utils/fast-safe-stringify/LICENSE b/js/src/utils/fast-safe-stringify/LICENSE new file mode 100644 index 000000000..bec900d11 --- /dev/null +++ b/js/src/utils/fast-safe-stringify/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2016 David Mark Clements +Copyright (c) 2017 David Mark Clements & Matteo Collina +Copyright (c) 2018 David Mark Clements, Matteo Collina & Ruben Bridgewater + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/js/src/utils/fast-safe-stringify/index.ts b/js/src/utils/fast-safe-stringify/index.ts new file mode 100644 index 000000000..7ae29d887 --- /dev/null +++ b/js/src/utils/fast-safe-stringify/index.ts @@ -0,0 +1,230 @@ +/* eslint-disable */ +// @ts-nocheck +var LIMIT_REPLACE_NODE = "[...]"; +var CIRCULAR_REPLACE_NODE = { result: "[Circular]" }; + +var arr = []; +var replacerStack = []; + +function defaultOptions() { + return { + depthLimit: Number.MAX_SAFE_INTEGER, + edgesLimit: Number.MAX_SAFE_INTEGER, + }; +} + +// Regular stringify +export function stringify(obj, replacer?, spacer?, options?) { + if (typeof options === "undefined") { + options = defaultOptions(); + } + + decirc(obj, "", 0, [], undefined, 0, options); + var res; + try { + if (replacerStack.length === 0) { + res = JSON.stringify(obj, replacer, spacer); + } else { + res = JSON.stringify(obj, replaceGetterValues(replacer), spacer); + } + } catch (_) { + return JSON.stringify( + "[unable to serialize, circular reference is too complex to analyze]" + ); + } finally { + while (arr.length !== 0) { + var part = arr.pop(); + if (part.length === 4) { + Object.defineProperty(part[0], part[1], part[3]); + } else { + part[0][part[1]] = part[2]; + } + } + } + return res; +} + +function setReplace(replace, val, k, parent) { + var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k); + if (propertyDescriptor.get !== undefined) { + if (propertyDescriptor.configurable) { + Object.defineProperty(parent, k, { value: replace }); + arr.push([parent, k, val, propertyDescriptor]); + } else { + replacerStack.push([val, k, replace]); + } + } else { + parent[k] = replace; + arr.push([parent, k, val]); + } +} + +function decirc(val, k, edgeIndex, stack, parent, depth, options) { + depth += 1; + var i; + if (typeof val === "object" && val !== null) { + for (i = 0; i < stack.length; i++) { + if (stack[i] === val) { + setReplace(CIRCULAR_REPLACE_NODE, val, k, parent); + return; + } + } + + if ( + typeof options.depthLimit !== "undefined" && + depth > options.depthLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent); + return; + } + + if ( + typeof options.edgesLimit !== "undefined" && + edgeIndex + 1 > options.edgesLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent); + return; + } + + stack.push(val); + // Optimize for Arrays. Big arrays could kill the performance otherwise! + if (Array.isArray(val)) { + for (i = 0; i < val.length; i++) { + decirc(val[i], i, i, stack, val, depth, options); + } + } else { + var keys = Object.keys(val); + for (i = 0; i < keys.length; i++) { + var key = keys[i]; + decirc(val[key], key, i, stack, val, depth, options); + } + } + stack.pop(); + } +} + +// Stable-stringify +function compareFunction(a, b) { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; +} + +function deterministicStringify(obj, replacer, spacer, options) { + if (typeof options === "undefined") { + options = defaultOptions(); + } + + var tmp = deterministicDecirc(obj, "", 0, [], undefined, 0, options) || obj; + var res; + try { + if (replacerStack.length === 0) { + res = JSON.stringify(tmp, replacer, spacer); + } else { + res = JSON.stringify(tmp, replaceGetterValues(replacer), spacer); + } + } catch (_) { + return JSON.stringify( + "[unable to serialize, circular reference is too complex to analyze]" + ); + } finally { + // Ensure that we restore the object as it was. + while (arr.length !== 0) { + var part = arr.pop(); + if (part.length === 4) { + Object.defineProperty(part[0], part[1], part[3]); + } else { + part[0][part[1]] = part[2]; + } + } + } + return res; +} + +function deterministicDecirc(val, k, edgeIndex, stack, parent, depth, options) { + depth += 1; + var i; + if (typeof val === "object" && val !== null) { + for (i = 0; i < stack.length; i++) { + if (stack[i] === val) { + setReplace(CIRCULAR_REPLACE_NODE, val, k, parent); + return; + } + } + try { + if (typeof val.toJSON === "function") { + return; + } + } catch (_) { + return; + } + + if ( + typeof options.depthLimit !== "undefined" && + depth > options.depthLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent); + return; + } + + if ( + typeof options.edgesLimit !== "undefined" && + edgeIndex + 1 > options.edgesLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent); + return; + } + + stack.push(val); + // Optimize for Arrays. Big arrays could kill the performance otherwise! + if (Array.isArray(val)) { + for (i = 0; i < val.length; i++) { + deterministicDecirc(val[i], i, i, stack, val, depth, options); + } + } else { + // Create a temporary object in the required way + var tmp = {}; + var keys = Object.keys(val).sort(compareFunction); + for (i = 0; i < keys.length; i++) { + var key = keys[i]; + deterministicDecirc(val[key], key, i, stack, val, depth, options); + tmp[key] = val[key]; + } + if (typeof parent !== "undefined") { + arr.push([parent, k, val]); + parent[k] = tmp; + } else { + return tmp; + } + } + stack.pop(); + } +} + +// wraps replacer function to handle values we couldn't replace +// and mark them as replaced value +function replaceGetterValues(replacer) { + replacer = + typeof replacer !== "undefined" + ? replacer + : function (k, v) { + return v; + }; + return function (key, val) { + if (replacerStack.length > 0) { + for (var i = 0; i < replacerStack.length; i++) { + var part = replacerStack[i]; + if (part[1] === key && part[0] === val) { + val = part[2]; + replacerStack.splice(i, 1); + break; + } + } + } + return replacer.call(this, key, val); + }; +} diff --git a/js/src/utils/serde.ts b/js/src/utils/serde.ts deleted file mode 100644 index 7fb155d3f..000000000 --- a/js/src/utils/serde.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const CIRCULAR_VALUE_REPLACEMENT_STRING = "[Circular]"; - -/** - * JSON.stringify version that handles circular references by replacing them - * with an object marking them as such ({ result: "[Circular]" }). - */ -export const stringifyForTracing = (value: any): string => { - const seen = new WeakSet(); - - const serializer = (_: string, value: any): any => { - if (typeof value === "object" && value !== null) { - if (seen.has(value)) { - return { - result: CIRCULAR_VALUE_REPLACEMENT_STRING, - }; - } - seen.add(value); - } - return value; - }; - return JSON.stringify(value, serializer); -};