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

fix(js): LangChain / traceable handoff #678

Merged
merged 31 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
42b5374
Add failing tests
dqbd May 9, 2024
72d92ca
Make sure we're passing the custom client in fromRunnableConfig as well
dqbd May 9, 2024
e510cf6
Add util method for converting RunTree to CallbackLike
dqbd May 9, 2024
0169043
Load parent run as well
dqbd May 9, 2024
74ad7de
Remove console.log
dqbd May 9, 2024
9ed6a45
Move RunnableTraceable for testing purposes
dqbd May 9, 2024
012035e
Infer tracingEnabled
dqbd May 9, 2024
cf2f022
Link nested langsmith found in @langchain/core
dqbd May 9, 2024
88feb02
Use fromRunnableConfig, fix invalid parent run
dqbd May 9, 2024
ee7fa33
Cleanup
dqbd May 9, 2024
c2f5d3d
Inline getCurrentRunTree
dqbd May 10, 2024
2efbc8f
Fix lint
dqbd May 10, 2024
3e08b0d
Split env detection to separate file
dqbd May 13, 2024
716cab3
Add optional @langchain/core dependency for backwards compatibility, …
dqbd May 13, 2024
c1e4381
Make sure we pass project name and example ID
dqbd May 13, 2024
dab8469
Remove the output parsing stuff
dqbd May 14, 2024
5b1db12
Split context in a separate entrypoint
dqbd May 14, 2024
04e6398
Remove type dependency on AsyncLocalStorage
dqbd May 14, 2024
06b1799
use singletons instead
dqbd May 14, 2024
dcf569e
Move main logic to singletons
dqbd May 15, 2024
b74cd98
Use Symbol.for instead
dqbd May 15, 2024
4f8bebe
Split langchain tests out
dqbd May 15, 2024
157685f
Add tests verifying stream and batch
dqbd May 15, 2024
a8e6e3b
Fix build
dqbd May 15, 2024
8c1f1ba
Remove hard resolutions
dqbd May 15, 2024
ed8c558
Remove package.json
dqbd May 15, 2024
24a99c8
Use env vars in run trees
dqbd May 16, 2024
3886649
Cleanup
dqbd May 16, 2024
0c7105b
Update yarn.lock
dqbd May 16, 2024
d8ba183
Code Review
dqbd May 17, 2024
ef19313
Inherit tracing enable if tracer is found
dqbd May 17, 2024
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
8 changes: 8 additions & 0 deletions js/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Chinook_Sqlite.sql
/schemas.js
/schemas.d.ts
/schemas.d.cts
/langchain.cjs
/langchain.js
/langchain.d.ts
/langchain.d.cts
/wrappers.cjs
/wrappers.js
/wrappers.d.ts
Expand All @@ -59,6 +63,10 @@ Chinook_Sqlite.sql
/wrappers/openai.js
/wrappers/openai.d.ts
/wrappers/openai.d.cts
/singletons/traceable.cjs
/singletons/traceable.js
/singletons/traceable.d.ts
/singletons/traceable.d.cts
/index.cjs
/index.js
/index.d.ts
Expand Down
34 changes: 32 additions & 2 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"schemas.js",
"schemas.d.ts",
"schemas.d.cts",
"langchain.cjs",
"langchain.js",
"langchain.d.ts",
"langchain.d.cts",
"wrappers.cjs",
"wrappers.js",
"wrappers.d.ts",
Expand All @@ -33,6 +37,10 @@
"wrappers/openai.js",
"wrappers/openai.d.ts",
"wrappers/openai.d.cts",
"singletons/traceable.cjs",
"singletons/traceable.js",
"singletons/traceable.d.ts",
"singletons/traceable.d.cts",
"index.cjs",
"index.js",
"index.d.ts",
Expand Down Expand Up @@ -109,11 +117,15 @@
"typescript": "^5.4.5"
},
"peerDependencies": {
"openai": "*"
"openai": "*",
"@langchain/core": "*"
},
"peerDependenciesMeta": {
"openai": {
"optional": true
},
"@langchain/core": {
"optional": true
}
},
"lint-staged": {
Expand Down Expand Up @@ -177,6 +189,15 @@
"import": "./schemas.js",
"require": "./schemas.cjs"
},
"./langchain": {
"types": {
"import": "./langchain.d.ts",
"require": "./langchain.d.cts",
"default": "./langchain.d.ts"
},
"import": "./langchain.js",
"require": "./langchain.cjs"
},
"./wrappers": {
"types": {
"import": "./wrappers.d.ts",
Expand All @@ -195,6 +216,15 @@
"import": "./wrappers/openai.js",
"require": "./wrappers/openai.cjs"
},
"./singletons/traceable": {
"types": {
"import": "./singletons/traceable.d.ts",
"require": "./singletons/traceable.d.cts",
"default": "./singletons/traceable.d.ts"
},
"import": "./singletons/traceable.js",
"require": "./singletons/traceable.cjs"
},
"./package.json": "./package.json"
}
}
}
2 changes: 2 additions & 0 deletions js/scripts/create-entrypoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ const entrypoints = {
traceable: "traceable",
evaluation: "evaluation/index",
schemas: "schemas",
langchain: "langchain",
dqbd marked this conversation as resolved.
Show resolved Hide resolved
wrappers: "wrappers/index",
"wrappers/openai": "wrappers/openai",
"singletons/traceable": "singletons/traceable",
};
const updateJsonFile = (relativePath, updateFunction) => {
const contents = fs.readFileSync(relativePath).toString();
Expand Down
16 changes: 16 additions & 0 deletions js/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getEnvironmentVariable } from "./utils/env.js";

export const isTracingEnabled = (tracingEnabled?: boolean): boolean => {
if (tracingEnabled !== undefined) {
return tracingEnabled;
}
const envVars = [
"LANGSMITH_TRACING_V2",
"LANGCHAIN_TRACING_V2",
"LANGSMITH_TRACING",
"LANGCHAIN_TRACING",
];
return Boolean(
dqbd marked this conversation as resolved.
Show resolved Hide resolved
envVars.find((envVar) => getEnvironmentVariable(envVar) === "true")
);
};
122 changes: 122 additions & 0 deletions js/src/langchain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { CallbackManager } from "@langchain/core/callbacks/manager";
import { LangChainTracer } from "@langchain/core/tracers/tracer_langchain";
import { Runnable, RunnableConfig } from "@langchain/core/runnables";
jacoblee93 marked this conversation as resolved.
Show resolved Hide resolved

import { RunTree } from "./run_trees.js";
import { Run } from "./schemas.js";
import {
TraceableFunction,
getCurrentRunTree,
isTraceableFunction,
} from "./traceable.js";

/**
* Converts the current run tree active within a traceable-wrapped function
* into a LangChain compatible callback manager. This is useful to handoff tracing
* from LangSmith to LangChain Runnables and LLMs.
*
* @param {RunTree | undefined} currentRunTree Current RunTree from within a traceable-wrapped function. If not provided, the current run tree will be inferred from AsyncLocalStorage.
* @returns {CallbackManager | undefined} Callback manager used by LangChain Runnable objects.
*/
export async function getLangchainCallbacks(
currentRunTree?: RunTree | undefined
) {
const runTree: RunTree | undefined = currentRunTree ?? getCurrentRunTree();
if (!runTree) return undefined;

// TODO: CallbackManager.configure() is only async due to LangChainTracer
// factory being unnecessarily async.
let callbacks = await CallbackManager.configure();
if (!callbacks && runTree.tracingEnabled) {
callbacks = new CallbackManager();
}

let langChainTracer = callbacks?.handlers.find(
(handler): handler is LangChainTracer =>
handler?.name === "langchain_tracer"
);

if (!langChainTracer && runTree.tracingEnabled) {
langChainTracer = new LangChainTracer();
callbacks?.addHandler(langChainTracer);
}

const runMap = new Map<string, Run>();

// find upward root run
let rootRun = runTree;
const rootVisited = new Set<string>();
while (rootRun.parent_run) {
if (rootVisited.has(rootRun.id)) break;
rootVisited.add(rootRun.id);
rootRun = rootRun.parent_run;
}

const queue = [rootRun];
const visited = new Set<string>();

while (queue.length > 0) {
const current = queue.shift();
if (!current || visited.has(current.id)) continue;
jacoblee93 marked this conversation as resolved.
Show resolved Hide resolved
visited.add(current.id);

runMap.set(current.id, current);
if (current.child_runs) {
queue.push(...current.child_runs);
}
}

if (callbacks != null) {
Object.assign(callbacks, { _parentRunId: runTree.id });
jacoblee93 marked this conversation as resolved.
Show resolved Hide resolved
}

if (langChainTracer != null) {
Object.assign(langChainTracer, {
runMap,
client: runTree.client,
projectName: runTree.project_name || langChainTracer.projectName,
exampleId: runTree.reference_example_id || langChainTracer.exampleId,
});
}

return callbacks;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyTraceableFunction = TraceableFunction<(...any: any[]) => any>;

/**
* RunnableTraceable is a Runnable that wraps a traceable function.
* This allows adding Langsmith traced functions into LangChain sequences.
*/
export class RunnableTraceable<RunInput, RunOutput> extends Runnable<
dqbd marked this conversation as resolved.
Show resolved Hide resolved
RunInput,
RunOutput
> {
lc_serializable = false;

lc_namespace = ["langchain_core", "runnables"];

protected func: AnyTraceableFunction;

constructor(fields: { func: AnyTraceableFunction }) {
super(fields);

if (!isTraceableFunction(fields.func)) {
throw new Error(
"RunnableTraceable requires a function that is wrapped in traceable higher-order function"
);
}

this.func = fields.func;
}

async invoke(input: RunInput, options?: Partial<RunnableConfig>) {
const [config] = this._getOptionsList(options ?? {}, 1);
return (await this.func(config, input)) as RunOutput;
}

static from(func: AnyTraceableFunction) {
return new RunnableTraceable({ func });
}
}
44 changes: 28 additions & 16 deletions js/src/run_trees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getRuntimeEnvironment,
} from "./utils/env.js";
import { Client } from "./client.js";
import { isTracingEnabled } from "./env.js";

const warnedMessages: Record<string, boolean> = {};

Expand Down Expand Up @@ -93,6 +94,7 @@ interface LangChainTracerLike extends TracerLike {
name: "langchain_tracer";
projectName: string;
getRun?: (id: string) => RunTree | undefined;
client: Client;
}

export class RunTree implements BaseRun {
Expand Down Expand Up @@ -157,44 +159,54 @@ export class RunTree implements BaseRun {
}

static fromRunnableConfig(
config: RunnableConfigLike,
parentConfig: RunnableConfigLike,
props: {
name: string;
tags?: string[];
metadata?: KVMap;
}
): RunTree {
// We only handle the callback manager case for now
const callbackManager = config?.callbacks as
const callbackManager = parentConfig?.callbacks as
| CallbackManagerLike
| undefined;
let parentRun: RunTree | undefined;
let projectName: string | undefined;
let client: Client | undefined;

if (callbackManager) {
const parentRunId = callbackManager?.getParentRunId?.() ?? "";
const langChainTracer = callbackManager?.handlers?.find(
(handler: TracerLike) => handler?.name == "langchain_tracer"
) as LangChainTracerLike | undefined;

parentRun = langChainTracer?.getRun?.(parentRunId);
projectName = langChainTracer?.projectName;
client = langChainTracer?.client;
}
const dedupedTags = [
...new Set((parentRun?.tags ?? []).concat(config?.tags ?? [])),
];
const dedupedMetadata = {
...parentRun?.extra?.metadata,
...config?.metadata,
};
const rt = new RunTree({
name: props?.name ?? "<lambda>",
parent_run: parentRun,
tags: dedupedTags,

const parentRunTree = new RunTree({
name: parentRun?.name ?? "<parent>",
id: parentRun?.id,
client,
tracingEnabled: isTracingEnabled(),
project_name: projectName,
tags: [
...new Set((parentRun?.tags ?? []).concat(parentConfig?.tags ?? [])),
],
extra: {
metadata: dedupedMetadata,
metadata: {
...parentRun?.extra?.metadata,
...parentConfig?.metadata,
},
},
project_name: projectName,
});
return rt;

return parentRunTree.createChild({
name: props?.name ?? "<lambda>",
tags: props.tags,
metadata: props.metadata,
});
}

private static getDefaultConfig(): object {
Expand Down
Loading
Loading