langsmith
Advanced tools
Comparing version 0.1.20 to 0.1.21
export { Client } from "./client.js"; | ||
export type { Dataset, Example, TracerSession, Run, Feedback, } from "./schemas.js"; | ||
export { RunTree, type RunTreeConfig } from "./run_trees.js"; | ||
export declare const __version__ = "0.1.20"; | ||
export declare const __version__ = "0.1.21"; |
export { Client } from "./client.js"; | ||
export { RunTree } from "./run_trees.js"; | ||
// Update using yarn bump-version | ||
export const __version__ = "0.1.20"; | ||
export const __version__ = "0.1.21"; |
@@ -68,3 +68,3 @@ import { BaseRun, KVMap } from "./schemas.js"; | ||
private static getDefaultConfig; | ||
createChild(config: RunTreeConfig): Promise<RunTree>; | ||
createChild(config: RunTreeConfig): RunTree; | ||
end(outputs?: KVMap, error?: string, endTime?: number): Promise<void>; | ||
@@ -71,0 +71,0 @@ private _convertToCreate; |
@@ -208,3 +208,3 @@ import * as uuid from "uuid"; | ||
} | ||
async createChild(config) { | ||
createChild(config) { | ||
const child = new RunTree({ | ||
@@ -220,5 +220,5 @@ ...config, | ||
async end(outputs, error, endTime = Date.now()) { | ||
this.outputs = outputs; | ||
this.error = error; | ||
this.end_time = endTime; | ||
this.outputs = this.outputs ?? outputs; | ||
this.error = this.error ?? error; | ||
this.end_time = this.end_time ?? endTime; | ||
} | ||
@@ -225,0 +225,0 @@ async _convertToCreate(run, excludeChildRuns = true) { |
import { RunTree, RunTreeConfig, RunnableConfigLike } from "./run_trees.js"; | ||
export type RunTreeLike = RunTree; | ||
type SmartPromise<T> = T extends AsyncGenerator ? T : T extends Promise<unknown> ? T : Promise<T>; | ||
type WrapArgReturnPair<Pair> = Pair extends [ | ||
@@ -7,5 +8,5 @@ infer Args extends any[], | ||
] ? { | ||
(...args: Args): Promise<Return>; | ||
(...args: [runTree: RunTreeLike, ...rest: Args]): Promise<Return>; | ||
(...args: [config: RunnableConfigLike, ...rest: Args]): Promise<Return>; | ||
(...args: Args): SmartPromise<Return>; | ||
(...args: [runTree: RunTreeLike, ...rest: Args]): SmartPromise<Return>; | ||
(...args: [config: RunnableConfigLike, ...rest: Args]): SmartPromise<Return>; | ||
} : never; | ||
@@ -50,2 +51,3 @@ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; | ||
aggregator?: (args: any[]) => any; | ||
argsConfigPath?: [number] | [number, string]; | ||
}): TraceableFunction<Func>; | ||
@@ -52,0 +54,0 @@ /** |
import { AsyncLocalStorage } from "async_hooks"; | ||
import { RunTree, isRunTree, isRunnableConfigLike, } from "./run_trees.js"; | ||
import { getEnvironmentVariable } from "./utils/env.js"; | ||
function isPromiseMethod(x) { | ||
if (x === "then" || x === "catch" || x === "finally") { | ||
return true; | ||
} | ||
return false; | ||
} | ||
const asyncLocalStorage = new AsyncLocalStorage(); | ||
@@ -33,10 +39,51 @@ const isAsyncIterable = (x) => x != null && | ||
export function traceable(wrappedFunc, config) { | ||
const { aggregator, ...runTreeConfig } = config ?? {}; | ||
const traceableFunc = async (...args) => { | ||
const { aggregator, argsConfigPath, ...runTreeConfig } = config ?? {}; | ||
const traceableFunc = (...args) => { | ||
let currentRunTree; | ||
let rawInputs; | ||
const ensuredConfig = { | ||
name: wrappedFunc.name || "<lambda>", | ||
...runTreeConfig, | ||
}; | ||
let ensuredConfig; | ||
try { | ||
let runtimeConfig; | ||
if (argsConfigPath) { | ||
const [index, path] = argsConfigPath; | ||
if (index === args.length - 1 && !path) { | ||
runtimeConfig = args.pop(); | ||
} | ||
else if (index <= args.length && | ||
typeof args[index] === "object" && | ||
args[index] !== null) { | ||
if (path) { | ||
const { [path]: extracted, ...rest } = args[index]; | ||
runtimeConfig = extracted; | ||
args[index] = rest; | ||
} | ||
else { | ||
runtimeConfig = args[index]; | ||
args.splice(index, 1); | ||
} | ||
} | ||
} | ||
ensuredConfig = { | ||
name: wrappedFunc.name || "<lambda>", | ||
...runTreeConfig, | ||
...runtimeConfig, | ||
tags: [ | ||
...new Set([ | ||
...(runTreeConfig?.tags ?? []), | ||
...(runtimeConfig?.tags ?? []), | ||
]), | ||
], | ||
metadata: { | ||
...runTreeConfig?.metadata, | ||
...runtimeConfig?.metadata, | ||
}, | ||
}; | ||
} | ||
catch (err) { | ||
console.warn(`Failed to extract runtime config from args for ${runTreeConfig?.name ?? wrappedFunc.name}`, err); | ||
ensuredConfig = { | ||
name: wrappedFunc.name || "<lambda>", | ||
...runTreeConfig, | ||
}; | ||
} | ||
const previousRunTree = asyncLocalStorage.getStore(); | ||
@@ -52,3 +99,3 @@ if (isRunTree(args[0])) { | ||
else if (previousRunTree !== undefined) { | ||
currentRunTree = await previousRunTree.createChild(ensuredConfig); | ||
currentRunTree = previousRunTree.createChild(ensuredConfig); | ||
rawInputs = args; | ||
@@ -78,39 +125,107 @@ } | ||
} | ||
const initialOutputs = currentRunTree?.outputs; | ||
const initialError = currentRunTree?.error; | ||
await currentRunTree?.postRun(); | ||
return new Promise((resolve, reject) => { | ||
void asyncLocalStorage.run(currentRunTree, async () => { | ||
try { | ||
const rawOutput = await wrappedFunc(...rawInputs); | ||
return asyncLocalStorage.run(currentRunTree, () => { | ||
const postRunPromise = currentRunTree?.postRun(); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
let returnValue; | ||
try { | ||
returnValue = wrappedFunc(...rawInputs); | ||
} | ||
catch (err) { | ||
returnValue = Promise.reject(err); | ||
} | ||
if (isAsyncIterable(returnValue)) { | ||
// eslint-disable-next-line no-inner-declarations | ||
async function* wrapOutputForTracing() { | ||
let finished = false; | ||
const chunks = []; | ||
try { | ||
for await (const chunk of returnValue) { | ||
chunks.push(chunk); | ||
yield chunk; | ||
} | ||
finished = true; | ||
} | ||
catch (e) { | ||
await currentRunTree?.end(undefined, String(e)); | ||
throw e; | ||
} | ||
finally { | ||
if (!finished) { | ||
await currentRunTree?.end(undefined, "Cancelled"); | ||
} | ||
let finalOutputs; | ||
if (aggregator !== undefined) { | ||
try { | ||
finalOutputs = await aggregator(chunks); | ||
} | ||
catch (e) { | ||
console.error(`[ERROR]: LangSmith aggregation failed: `, e); | ||
finalOutputs = chunks; | ||
} | ||
} | ||
else { | ||
finalOutputs = chunks; | ||
} | ||
if (typeof finalOutputs === "object" && | ||
!Array.isArray(finalOutputs)) { | ||
await currentRunTree?.end(finalOutputs); | ||
} | ||
else { | ||
await currentRunTree?.end({ outputs: finalOutputs }); | ||
} | ||
await postRunPromise; | ||
await currentRunTree?.patchRun(); | ||
} | ||
} | ||
return wrapOutputForTracing(); | ||
} | ||
const tracedPromise = new Promise((resolve, reject) => { | ||
Promise.resolve(returnValue) | ||
.then( | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async (rawOutput) => { | ||
if (isAsyncIterable(rawOutput)) { | ||
// eslint-disable-next-line no-inner-declarations | ||
async function* wrapOutputForTracing() { | ||
let finished = false; | ||
const chunks = []; | ||
// TypeScript thinks this is unsafe | ||
for await (const chunk of rawOutput) { | ||
chunks.push(chunk); | ||
yield chunk; | ||
try { | ||
// TypeScript thinks this is unsafe | ||
for await (const chunk of rawOutput) { | ||
chunks.push(chunk); | ||
yield chunk; | ||
} | ||
finished = true; | ||
} | ||
let finalOutputs; | ||
if (aggregator !== undefined) { | ||
try { | ||
finalOutputs = await aggregator(chunks); | ||
catch (e) { | ||
await currentRunTree?.end(undefined, String(e)); | ||
throw e; | ||
} | ||
finally { | ||
if (!finished) { | ||
await currentRunTree?.end(undefined, "Cancelled"); | ||
} | ||
catch (e) { | ||
console.error(`[ERROR]: LangSmith aggregation failed: `, e); | ||
let finalOutputs; | ||
if (aggregator !== undefined) { | ||
try { | ||
finalOutputs = await aggregator(chunks); | ||
} | ||
catch (e) { | ||
console.error(`[ERROR]: LangSmith aggregation failed: `, e); | ||
finalOutputs = chunks; | ||
} | ||
} | ||
else { | ||
finalOutputs = chunks; | ||
} | ||
if (typeof finalOutputs === "object" && | ||
!Array.isArray(finalOutputs)) { | ||
await currentRunTree?.end(finalOutputs); | ||
} | ||
else { | ||
await currentRunTree?.end({ outputs: finalOutputs }); | ||
} | ||
await postRunPromise; | ||
await currentRunTree?.patchRun(); | ||
} | ||
else { | ||
finalOutputs = chunks; | ||
} | ||
if (typeof finalOutputs === "object" && | ||
!Array.isArray(finalOutputs)) { | ||
await currentRunTree?.end(finalOutputs); | ||
} | ||
else { | ||
await currentRunTree?.end({ outputs: finalOutputs }); | ||
} | ||
await currentRunTree?.patchRun(); | ||
} | ||
@@ -120,30 +235,33 @@ return resolve(wrapOutputForTracing()); | ||
else { | ||
const outputs = isKVMap(rawOutput) | ||
? rawOutput | ||
: { outputs: rawOutput }; | ||
if (initialOutputs === currentRunTree?.outputs) { | ||
await currentRunTree?.end(outputs); | ||
try { | ||
await currentRunTree?.end(isKVMap(rawOutput) ? rawOutput : { outputs: rawOutput }); | ||
await postRunPromise; | ||
await currentRunTree?.patchRun(); | ||
} | ||
else { | ||
if (currentRunTree !== undefined) { | ||
currentRunTree.end_time = Date.now(); | ||
} | ||
finally { | ||
// eslint-disable-next-line no-unsafe-finally | ||
return rawOutput; | ||
} | ||
await currentRunTree?.patchRun(); | ||
return resolve(rawOutput); | ||
} | ||
} | ||
catch (error) { | ||
if (initialError === currentRunTree?.error) { | ||
await currentRunTree?.end(initialOutputs, String(error)); | ||
} | ||
else { | ||
if (currentRunTree !== undefined) { | ||
currentRunTree.end_time = Date.now(); | ||
} | ||
} | ||
}, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async (error) => { | ||
await currentRunTree?.end(undefined, String(error)); | ||
await postRunPromise; | ||
await currentRunTree?.patchRun(); | ||
reject(error); | ||
} | ||
throw error; | ||
}) | ||
.then(resolve, reject); | ||
}); | ||
if (typeof returnValue !== "object" || returnValue === null) { | ||
return tracedPromise; | ||
} | ||
return new Proxy(returnValue, { | ||
get(target, prop, receiver) { | ||
if (isPromiseMethod(prop)) { | ||
return tracedPromise[prop].bind(tracedPromise); | ||
} | ||
return Reflect.get(target, prop, receiver); | ||
}, | ||
}); | ||
}); | ||
@@ -150,0 +268,0 @@ }; |
import pRetry from "p-retry"; | ||
import PQueueMod from "p-queue"; | ||
const STATUS_NO_RETRY = [ | ||
400, | ||
401, | ||
403, | ||
404, | ||
405, | ||
406, | ||
407, | ||
400, // Bad Request | ||
401, // Unauthorized | ||
403, // Forbidden | ||
404, // Not Found | ||
405, // Method Not Allowed | ||
406, // Not Acceptable | ||
407, // Proxy Authentication Required | ||
408, // Request Timeout | ||
@@ -57,4 +57,12 @@ ]; | ||
this.maxRetries = params.maxRetries ?? 6; | ||
const PQueue = "default" in PQueueMod ? PQueueMod.default : PQueueMod; | ||
this.queue = new PQueue({ concurrency: this.maxConcurrency }); | ||
if ("default" in PQueueMod) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
this.queue = new PQueueMod.default({ | ||
concurrency: this.maxConcurrency, | ||
}); | ||
} | ||
else { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
this.queue = new PQueueMod({ concurrency: this.maxConcurrency }); | ||
} | ||
this.onFailedResponseHook = params?.onFailedResponseHook; | ||
@@ -61,0 +69,0 @@ } |
@@ -1,2 +0,3 @@ | ||
import type { OpenAI } from "openai"; | ||
import { OpenAI } from "openai"; | ||
import type { APIPromise } from "openai/core"; | ||
import type { Client, RunTreeConfig } from "../index.js"; | ||
@@ -15,27 +16,25 @@ import { type RunnableConfigLike } from "../run_trees.js"; | ||
}; | ||
type PatchedOpenAIClient<T extends OpenAIType> = { | ||
[P in keyof T]: T[P]; | ||
} & { | ||
chat: { | ||
completions: { | ||
type PatchedOpenAIClient<T extends OpenAIType> = T & { | ||
chat: T["chat"] & { | ||
completions: T["chat"]["completions"] & { | ||
create: { | ||
(arg: OpenAI.ChatCompletionCreateParamsStreaming, arg2?: OpenAI.RequestOptions & { | ||
langsmithExtra?: RunnableConfigLike | RunTreeLike; | ||
}): Promise<AsyncGenerator<OpenAI.ChatCompletionChunk>>; | ||
}): APIPromise<AsyncGenerator<OpenAI.ChatCompletionChunk>>; | ||
} & { | ||
(arg: OpenAI.ChatCompletionCreateParamsNonStreaming, arg2?: OpenAI.RequestOptions & { | ||
langsmithExtra?: RunnableConfigLike | RunTreeLike; | ||
}): Promise<OpenAI.ChatCompletionChunk>; | ||
}): APIPromise<OpenAI.ChatCompletionChunk>; | ||
}; | ||
}; | ||
}; | ||
completions: { | ||
completions: T["completions"] & { | ||
create: { | ||
(arg: OpenAI.CompletionCreateParamsStreaming, arg2?: OpenAI.RequestOptions & { | ||
langsmithExtra?: RunnableConfigLike | RunTreeLike; | ||
}): Promise<AsyncGenerator<OpenAI.Completion>>; | ||
}): APIPromise<AsyncGenerator<OpenAI.Completion>>; | ||
} & { | ||
(arg: OpenAI.CompletionCreateParamsNonStreaming, arg2?: OpenAI.RequestOptions & { | ||
langsmithExtra?: RunnableConfigLike | RunTreeLike; | ||
}): Promise<OpenAI.Completion>; | ||
}): APIPromise<OpenAI.Completion>; | ||
}; | ||
@@ -42,0 +41,0 @@ }; |
import { traceable } from "../traceable.js"; | ||
function _combineChatCompletionChoices(choices) { | ||
function _combineChatCompletionChoices(choices | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
) { | ||
const reversedChoices = choices.slice().reverse(); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const message = { | ||
@@ -73,22 +76,39 @@ role: "assistant", | ||
} | ||
async function extractLangSmithExtraAndCall(openAIMethod, args, defaultRunConfig) { | ||
if (args[1]?.langsmithExtra !== undefined) { | ||
const { langsmithExtra, ...openAIOptions } = args[1]; | ||
const wrappedMethod = traceable(openAIMethod, { | ||
...defaultRunConfig, | ||
...langsmithExtra, | ||
}); | ||
const finalArgs = [args[0]]; | ||
if (args.length > 2) { | ||
finalArgs.push(openAIOptions); | ||
finalArgs.push(args.slice(2)); | ||
const chatAggregator = (chunks) => { | ||
if (!chunks || chunks.length === 0) { | ||
return { choices: [{ message: { role: "assistant", content: "" } }] }; | ||
} | ||
const choicesByIndex = {}; | ||
for (const chunk of chunks) { | ||
for (const choice of chunk.choices) { | ||
if (choicesByIndex[choice.index] === undefined) { | ||
choicesByIndex[choice.index] = []; | ||
} | ||
choicesByIndex[choice.index].push(choice); | ||
} | ||
else if (Object.keys(openAIOptions).length !== 0) { | ||
finalArgs.push(openAIOptions); | ||
} | ||
const aggregatedOutput = chunks[chunks.length - 1]; | ||
aggregatedOutput.choices = Object.values(choicesByIndex).map((choices) => _combineChatCompletionChoices(choices)); | ||
return aggregatedOutput; | ||
}; | ||
const textAggregator = (allChunks | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
) => { | ||
if (allChunks.length === 0) { | ||
return { choices: [{ text: "" }] }; | ||
} | ||
const allContent = []; | ||
for (const chunk of allChunks) { | ||
const content = chunk.choices[0].text; | ||
if (content != null) { | ||
allContent.push(content); | ||
} | ||
return wrappedMethod(...finalArgs); | ||
} | ||
const wrappedMethod = traceable(openAIMethod, defaultRunConfig); | ||
return wrappedMethod(...args); | ||
} | ||
const content = allContent.join(""); | ||
const aggregatedOutput = allChunks[allChunks.length - 1]; | ||
aggregatedOutput.choices = [ | ||
{ ...aggregatedOutput.choices[0], text: content }, | ||
]; | ||
return aggregatedOutput; | ||
}; | ||
/** | ||
@@ -119,57 +139,16 @@ * Wraps an OpenAI client's completion methods, enabling automatic LangSmith | ||
export const wrapOpenAI = (openai, options) => { | ||
const originalChatCompletionsFn = openai.chat.completions.create.bind(openai.chat.completions); | ||
openai.chat.completions.create = async (...args) => { | ||
const aggregator = (chunks) => { | ||
if (!chunks || chunks.length === 0) { | ||
return { choices: [{ message: { role: "assistant", content: "" } }] }; | ||
} | ||
const choicesByIndex = {}; | ||
for (const chunk of chunks) { | ||
for (const choice of chunk.choices) { | ||
if (choicesByIndex[choice.index] === undefined) { | ||
choicesByIndex[choice.index] = []; | ||
} | ||
choicesByIndex[choice.index].push(choice); | ||
} | ||
} | ||
const aggregatedOutput = chunks[chunks.length - 1]; | ||
aggregatedOutput.choices = Object.values(choicesByIndex).map((choices) => _combineChatCompletionChoices(choices)); | ||
return aggregatedOutput; | ||
}; | ||
const defaultRunConfig = { | ||
name: "ChatOpenAI", | ||
run_type: "llm", | ||
aggregator, | ||
...options, | ||
}; | ||
return extractLangSmithExtraAndCall(originalChatCompletionsFn, args, defaultRunConfig); | ||
}; | ||
const originalCompletionsFn = openai.completions.create.bind(openai.chat.completions); | ||
openai.completions.create = async (...args) => { | ||
const aggregator = (allChunks) => { | ||
if (allChunks.length === 0) { | ||
return { choices: [{ text: "" }] }; | ||
} | ||
const allContent = []; | ||
for (const chunk of allChunks) { | ||
const content = chunk.choices[0].text; | ||
if (content != null) { | ||
allContent.push(content); | ||
} | ||
} | ||
const content = allContent.join(""); | ||
const aggregatedOutput = allChunks[allChunks.length - 1]; | ||
aggregatedOutput.choices = [ | ||
{ ...aggregatedOutput.choices[0], text: content }, | ||
]; | ||
return aggregatedOutput; | ||
}; | ||
const defaultRunConfig = { | ||
name: "OpenAI", | ||
run_type: "llm", | ||
aggregator, | ||
...options, | ||
}; | ||
return extractLangSmithExtraAndCall(originalCompletionsFn, args, defaultRunConfig); | ||
}; | ||
openai.chat.completions.create = traceable(openai.chat.completions.create.bind(openai.chat.completions), { | ||
name: "ChatOpenAI", | ||
run_type: "llm", | ||
aggregator: chatAggregator, | ||
argsConfigPath: [1, "langsmithExtra"], | ||
...options, | ||
}); | ||
openai.completions.create = traceable(openai.completions.create.bind(openai.completions), { | ||
name: "OpenAI", | ||
run_type: "llm", | ||
aggregator: textAggregator, | ||
argsConfigPath: [1, "langsmithExtra"], | ||
...options, | ||
}); | ||
return openai; | ||
@@ -182,3 +161,3 @@ }; | ||
if (typeof originalValue === "function") { | ||
return traceable(originalValue.bind(target), Object.assign({ name: [runName, propKey.toString()].join("."), run_type: "llm" }, options?.client)); | ||
return traceable(originalValue.bind(target), Object.assign({ name: [runName, propKey.toString()].join("."), run_type: "llm" }, options)); | ||
} | ||
@@ -185,0 +164,0 @@ else if (originalValue != null && |
{ | ||
"name": "langsmith", | ||
"version": "0.1.20", | ||
"version": "0.1.21", | ||
"description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.", | ||
@@ -50,4 +50,4 @@ "packageManager": "yarn@1.22.19", | ||
"clean": "rm -rf dist/ && node scripts/create-entrypoints.js clean", | ||
"build:esm": "tsc --outDir dist/ && rm -rf dist/tests dist/**/tests", | ||
"build:cjs": "tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs", | ||
"build:esm": "rm -f src/package.json && tsc --outDir dist/ && rm -rf dist/tests dist/**/tests", | ||
"build:cjs": "echo '{}' > src/package.json && tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs src/package.json", | ||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests --testPathIgnorePatterns='\\.int\\.test.[tj]s' --testTimeout 30000", | ||
@@ -108,3 +108,3 @@ "test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=\\.int\\.test.ts --testTimeout 100000", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^5.0.4" | ||
"typescript": "^5.4.5" | ||
}, | ||
@@ -111,0 +111,0 @@ "peerDependencies": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
293001
7203