From dc0028379993881e0d2ad9ef602b8a5b995804c6 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sun, 3 Mar 2024 12:50:35 -0800 Subject: [PATCH 1/6] Add generic wrapClient method --- js/package.json | 3 +- js/src/client.ts | 1 + js/src/tests/wrapped_sdk.int.test.ts | 85 ++++++++++++++++++++++++++++ js/src/wrappers.ts | 64 +++++++++++++++++++-- js/yarn.lock | 15 +++++ 5 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 js/src/tests/wrapped_sdk.int.test.ts diff --git a/js/package.json b/js/package.json index d028e54e8..e24cf2d95 100644 --- a/js/package.json +++ b/js/package.json @@ -67,6 +67,7 @@ }, "homepage": "https://github.com/langchain-ai/langsmith-sdk#readme", "devDependencies": { + "@anthropic-ai/sdk": "^0.14.1", "@babel/preset-env": "^7.22.4", "@jest/globals": "^29.5.0", "@langchain/core": "^0.1.32", @@ -141,4 +142,4 @@ }, "./package.json": "./package.json" } -} \ No newline at end of file +} diff --git a/js/src/client.ts b/js/src/client.ts index d06f51800..354363485 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -853,6 +853,7 @@ export class Client { signal: AbortSignal.timeout(this.timeout_ms), } ); + console.log(response); await raiseForStatus(response, "batch create run"); } diff --git a/js/src/tests/wrapped_sdk.int.test.ts b/js/src/tests/wrapped_sdk.int.test.ts new file mode 100644 index 000000000..6e7c204be --- /dev/null +++ b/js/src/tests/wrapped_sdk.int.test.ts @@ -0,0 +1,85 @@ +// import { jest } from "@jest/globals"; +import { Anthropic } from "@anthropic-ai/sdk"; +import { wrapClient } from "../wrappers.js"; +import { Client } from "../client.js"; + +test.concurrent("chat.completions", async () => { + const client = new Client(); + // const callSpy = jest + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // .spyOn((client as any).caller, "call") + // .mockResolvedValue({ ok: true, text: () => "" }); + + const originalClient = new Anthropic(); + const patchedClient = wrapClient(new Anthropic(), { client }); + + // invoke + const original = await originalClient.messages.create({ + messages: [ + { + role: "user", + content: `What is 1 + 1? Respond only with "2" and nothing else.`, + }, + ], + model: "claude-2.1", + max_tokens: 1024, + }); + + const patched = await patchedClient.messages.create({ + messages: [ + { + role: "user", + content: `What is 1 + 1? Respond only with "2" and nothing else.`, + }, + ], + model: "claude-2.1", + max_tokens: 1024, + }); + + expect(patched.content).toEqual(original.content); + + // stream + const originalStream = await originalClient.messages.create({ + messages: [ + { + role: "user", + content: `What is 1 + 1? Respond only with "2" and nothing else.`, + }, + ], + model: "claude-2.1", + max_tokens: 1024, + stream: true, + }); + + const originalChunks = []; + for await (const chunk of originalStream) { + if (chunk.type === "message_delta") { + originalChunks.push(chunk.delta); + } + } + + const patchedStream = await patchedClient.messages.create({ + messages: [ + { + role: "user", + content: `What is 1 + 1? Respond only with "2" and nothing else.`, + }, + ], + model: "claude-2.1", + max_tokens: 1024, + stream: true, + }); + + const patchedChunks = []; + for await (const chunk of patchedStream) { + if (chunk.type === "message_delta") { + patchedChunks.push(chunk.delta); + } + } + + expect(patchedChunks).toEqual(originalChunks); + // for (const call of callSpy.mock.calls) { + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // expect((call[2] as any)["method"]).toBe("POST"); + // } +}); diff --git a/js/src/wrappers.ts b/js/src/wrappers.ts index 9a25c8d6b..378448951 100644 --- a/js/src/wrappers.ts +++ b/js/src/wrappers.ts @@ -1,18 +1,26 @@ -import type { OpenAI } from "openai"; import type { Client } from "./index.js"; import { traceable } from "./traceable.js"; -export const wrapOpenAI = ( - openai: OpenAI, +type OpenAIType = { + chat: { + completions: { + create: (...args: any[]) => any; + }; + }; + completions: { + create: (...args: any[]) => any; + }; +}; + +export const wrapOpenAI = ( + openai: T, options?: { client?: Client } -): OpenAI => { - // @ts-expect-error Promise> != APIPromise<...> +): T => { openai.chat.completions.create = traceable( openai.chat.completions.create.bind(openai.chat.completions), Object.assign({ name: "ChatOpenAI", run_type: "llm" }, options?.client) ); - // @ts-expect-error Promise> != APIPromise<...> openai.completions.create = traceable( openai.completions.create.bind(openai.completions), Object.assign({ name: "OpenAI", run_type: "llm" }, options?.client) @@ -20,3 +28,47 @@ export const wrapOpenAI = ( return openai; }; + +const _wrapClient = ( + sdk: T, + runName: string, + options?: { client?: Client } +): T => { + return new Proxy(sdk, { + get(target, propKey, receiver) { + const originalValue = target[propKey as keyof T]; + if (typeof originalValue === "function") { + return traceable( + originalValue.bind(target), + Object.assign( + { name: [runName, propKey.toString()].join("."), run_type: "llm" }, + options?.client + ) + ); + } else if ( + originalValue != null && + !Array.isArray(originalValue) && + // eslint-disable-next-line no-instanceof/no-instanceof + !(originalValue instanceof Date) && + typeof originalValue === "object" + ) { + return _wrapClient( + originalValue, + [runName, propKey.toString()].join("."), + options + ); + } else { + return Reflect.get(target, propKey, receiver); + } + }, + }); +}; + +export const wrapClient = ( + sdk: T, + options?: { client?: Client; runName?: string } +): T => { + return _wrapClient(sdk, options?.runName ?? sdk.constructor?.name, { + client: options?.client, + }); +}; diff --git a/js/yarn.lock b/js/yarn.lock index 829a88720..44b944498 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -10,6 +10,21 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@anthropic-ai/sdk@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.14.1.tgz#85df82c265574a7ff9c49e63e481d6c4f3c44f54" + integrity sha512-/o0+6ijSF0WSxnzQ0GUZPKaxOE0y1dqAn9gM9KPU7hc/tqiI4lzCYqe/EFSEw8pFONgYi1IjcvevYjgOOc2vpg== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + digest-fetch "^1.3.0" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + web-streams-polyfill "^3.2.1" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.21.4": version "7.21.4" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz" From b519a4d829f4edaf8d8bc69bda951db876c3e66c Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sun, 3 Mar 2024 12:59:41 -0800 Subject: [PATCH 2/6] Remove log --- js/src/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/js/src/client.ts b/js/src/client.ts index 354363485..d06f51800 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -853,7 +853,6 @@ export class Client { signal: AbortSignal.timeout(this.timeout_ms), } ); - console.log(response); await raiseForStatus(response, "batch create run"); } From 01919305e503ad576ca5827181c225c186de738b Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Tue, 5 Mar 2024 11:08:20 -0800 Subject: [PATCH 3/6] Add docs --- js/README.md | 65 ++++++++++++++++++++++++++++ js/src/tests/wrapped_sdk.int.test.ts | 22 +++++----- js/src/wrappers.ts | 20 ++++++++- 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/js/README.md b/js/README.md index 7c9ad7732..4934bd5f9 100644 --- a/js/README.md +++ b/js/README.md @@ -292,6 +292,71 @@ export async function POST(req: Request) { See the [AI SDK docs](https://sdk.vercel.ai/docs) for more examples. +```ts +import { traceable } from "langsmith/traceable"; +import { OpenAIStream, StreamingTextResponse } from "ai"; + +// Note: There are no types for the Mistral API client yet. +import MistralClient from "@mistralai/mistralai"; + +const client = new MistralClient(process.env.MISTRAL_API_KEY || ""); + +export async function POST(req: Request) { + // Extract the `messages` from the body of the request + const { messages } = await req.json(); + + const mistralChatStream = traceable(client.chatStream.bind(client), { + name: "Mistral Stream", + run_type: "llm", + }); + + const response = await mistralChatStream({ + model: "mistral-tiny", + maxTokens: 1000, + messages, + }); + + // Convert the response into a friendly text-stream. The Mistral client responses are + // compatible with the Vercel AI SDK OpenAIStream adapter. + const stream = OpenAIStream(response as any); + + // Respond with the stream + return new StreamingTextResponse(stream); +} +``` + +## Arbitrary SDKs + +You can use the generic `wrapSDK` method to add tracing for arbitrary SDKs. + +Do note that this will trace ALL methods in the SDK, not just chat completion endpoints. +If the SDK you are wrapping has other methods, we recommend using it for only LLM calls. + +Here's an example using the Anthropic SDK: + +```ts +import { wrapSDK } from "langsmith/wrappers"; +import { Anthropic } from "@anthropic-ai/sdk"; + +const originalSDK = new Anthropic(); +const sdkWithTracing = wrapSDK(originalSDK); + +const response = await sdkWithTracing.messages.create({ + messages: [ + { + role: "user", + content: `What is 1 + 1? Respond only with "2" and nothing else.`, + }, + ], + model: "claude-2.1", + max_tokens: 1024, +}); +``` + +:::tip +[Click here](https://smith.langchain.com/public/3f84efd4-ae7c-49ef-bb4a-20d7ec61063d/r) to see an example LangSmith trace of the above. +::: + #### Alternatives: **Log traces using a RunTree.** A RunTree tracks your application. Each RunTree object is required to have a name and run_type. These and other important attributes are as follows: diff --git a/js/src/tests/wrapped_sdk.int.test.ts b/js/src/tests/wrapped_sdk.int.test.ts index 6e7c204be..2dc420314 100644 --- a/js/src/tests/wrapped_sdk.int.test.ts +++ b/js/src/tests/wrapped_sdk.int.test.ts @@ -1,17 +1,17 @@ -// import { jest } from "@jest/globals"; +import { jest } from "@jest/globals"; import { Anthropic } from "@anthropic-ai/sdk"; -import { wrapClient } from "../wrappers.js"; +import { wrapSDK } from "../wrappers.js"; import { Client } from "../client.js"; test.concurrent("chat.completions", async () => { const client = new Client(); - // const callSpy = jest - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // .spyOn((client as any).caller, "call") - // .mockResolvedValue({ ok: true, text: () => "" }); + const callSpy = jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn((client as any).caller, "call") + .mockResolvedValue({ ok: true, text: () => "" }); const originalClient = new Anthropic(); - const patchedClient = wrapClient(new Anthropic(), { client }); + const patchedClient = wrapSDK(new Anthropic(), { client }); // invoke const original = await originalClient.messages.create({ @@ -78,8 +78,8 @@ test.concurrent("chat.completions", async () => { } expect(patchedChunks).toEqual(originalChunks); - // for (const call of callSpy.mock.calls) { - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // expect((call[2] as any)["method"]).toBe("POST"); - // } + for (const call of callSpy.mock.calls) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((call[2] as any)["method"]).toBe("POST"); + } }); diff --git a/js/src/wrappers.ts b/js/src/wrappers.ts index 378448951..3011fa631 100644 --- a/js/src/wrappers.ts +++ b/js/src/wrappers.ts @@ -12,6 +12,13 @@ type OpenAIType = { }; }; +/** + * Wraps an OpenAI client's completion methods, enabling automatic LangSmith + * tracing. Method signatures are unchanged. + * @param openai An OpenAI client instance. + * @param options LangSmith options. + * @returns + */ export const wrapOpenAI = ( openai: T, options?: { client?: Client } @@ -64,7 +71,18 @@ const _wrapClient = ( }); }; -export const wrapClient = ( +/** + * Wrap an arbitrary SDK, enabling automatic LangSmith tracing. + * Method signatures are unchanged. + * + * Note that this will wrap and trace ALL SDK methods, not just + * LLM completion methods. If the passed SDK contains other methods, + * we recommend using the wrapped instance for LLM calls only. + * @param sdk An arbitrary SDK instance. + * @param options LangSmith options. + * @returns + */ +export const wrapSDK = ( sdk: T, options?: { client?: Client; runName?: string } ): T => { From 31765acf31c8893027ec28c9c1ddcd40c4bcb5d6 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Tue, 5 Mar 2024 11:10:03 -0800 Subject: [PATCH 4/6] Fix --- js/README.md | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/js/README.md b/js/README.md index 4934bd5f9..aa2d50cb3 100644 --- a/js/README.md +++ b/js/README.md @@ -292,39 +292,6 @@ export async function POST(req: Request) { See the [AI SDK docs](https://sdk.vercel.ai/docs) for more examples. -```ts -import { traceable } from "langsmith/traceable"; -import { OpenAIStream, StreamingTextResponse } from "ai"; - -// Note: There are no types for the Mistral API client yet. -import MistralClient from "@mistralai/mistralai"; - -const client = new MistralClient(process.env.MISTRAL_API_KEY || ""); - -export async function POST(req: Request) { - // Extract the `messages` from the body of the request - const { messages } = await req.json(); - - const mistralChatStream = traceable(client.chatStream.bind(client), { - name: "Mistral Stream", - run_type: "llm", - }); - - const response = await mistralChatStream({ - model: "mistral-tiny", - maxTokens: 1000, - messages, - }); - - // Convert the response into a friendly text-stream. The Mistral client responses are - // compatible with the Vercel AI SDK OpenAIStream adapter. - const stream = OpenAIStream(response as any); - - // Respond with the stream - return new StreamingTextResponse(stream); -} -``` - ## Arbitrary SDKs You can use the generic `wrapSDK` method to add tracing for arbitrary SDKs. From 81b26e8fe1c3788ca7ddfef7d0ba69a7a3023ef6 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Tue, 5 Mar 2024 11:12:43 -0800 Subject: [PATCH 5/6] Claude-3 --- js/README.md | 4 ++-- js/package.json | 2 +- js/yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/js/README.md b/js/README.md index aa2d50cb3..158a0157a 100644 --- a/js/README.md +++ b/js/README.md @@ -315,13 +315,13 @@ const response = await sdkWithTracing.messages.create({ content: `What is 1 + 1? Respond only with "2" and nothing else.`, }, ], - model: "claude-2.1", + model: "claude-3-sonnet-20240229", max_tokens: 1024, }); ``` :::tip -[Click here](https://smith.langchain.com/public/3f84efd4-ae7c-49ef-bb4a-20d7ec61063d/r) to see an example LangSmith trace of the above. +[Click here](https://smith.langchain.com/public/0e7248af-bbed-47cf-be9f-5967fea1dec1/r) to see an example LangSmith trace of the above. ::: #### Alternatives: **Log traces using a RunTree.** diff --git a/js/package.json b/js/package.json index c630c2cbf..3809cc9f3 100644 --- a/js/package.json +++ b/js/package.json @@ -64,7 +64,7 @@ }, "homepage": "https://github.com/langchain-ai/langsmith-sdk#readme", "devDependencies": { - "@anthropic-ai/sdk": "^0.14.1", + "@anthropic-ai/sdk": "^0.16.1", "@babel/preset-env": "^7.22.4", "@jest/globals": "^29.5.0", "@langchain/core": "^0.1.32", diff --git a/js/yarn.lock b/js/yarn.lock index 44b944498..d61693513 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -10,10 +10,10 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@anthropic-ai/sdk@^0.14.1": - version "0.14.1" - resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.14.1.tgz#85df82c265574a7ff9c49e63e481d6c4f3c44f54" - integrity sha512-/o0+6ijSF0WSxnzQ0GUZPKaxOE0y1dqAn9gM9KPU7hc/tqiI4lzCYqe/EFSEw8pFONgYi1IjcvevYjgOOc2vpg== +"@anthropic-ai/sdk@^0.16.1": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.16.1.tgz#7472c42389d9a5323c20afa53995e1c3b922b95d" + integrity sha512-vHgvfWEyFy5ktqam56Nrhv8MVa7EJthsRYNi+1OrFFfyrj9tR2/aji1QbVbQjYU/pPhPFaYrdCEC/MLPFrmKwA== dependencies: "@types/node" "^18.11.18" "@types/node-fetch" "^2.6.4" From c3e5d9adde1ec4eaae9ee9f58c318181cc9eef0a Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Tue, 5 Mar 2024 11:24:13 -0800 Subject: [PATCH 6/6] Use OpenAI for integration test --- js/package.json | 1 - js/src/tests/wrapped_sdk.int.test.ts | 78 +++++++++++----------------- js/yarn.lock | 15 ------ 3 files changed, 29 insertions(+), 65 deletions(-) diff --git a/js/package.json b/js/package.json index 3809cc9f3..51156259a 100644 --- a/js/package.json +++ b/js/package.json @@ -64,7 +64,6 @@ }, "homepage": "https://github.com/langchain-ai/langsmith-sdk#readme", "devDependencies": { - "@anthropic-ai/sdk": "^0.16.1", "@babel/preset-env": "^7.22.4", "@jest/globals": "^29.5.0", "@langchain/core": "^0.1.32", diff --git a/js/src/tests/wrapped_sdk.int.test.ts b/js/src/tests/wrapped_sdk.int.test.ts index 2dc420314..70d086f5a 100644 --- a/js/src/tests/wrapped_sdk.int.test.ts +++ b/js/src/tests/wrapped_sdk.int.test.ts @@ -1,5 +1,5 @@ import { jest } from "@jest/globals"; -import { Anthropic } from "@anthropic-ai/sdk"; +import { OpenAI } from "openai"; import { wrapSDK } from "../wrappers.js"; import { Client } from "../client.js"; @@ -10,74 +10,54 @@ test.concurrent("chat.completions", async () => { .spyOn((client as any).caller, "call") .mockResolvedValue({ ok: true, text: () => "" }); - const originalClient = new Anthropic(); - const patchedClient = wrapSDK(new Anthropic(), { client }); + const originalClient = new OpenAI(); + const patchedClient = wrapSDK(new OpenAI(), { client }); // invoke - const original = await originalClient.messages.create({ - messages: [ - { - role: "user", - content: `What is 1 + 1? Respond only with "2" and nothing else.`, - }, - ], - model: "claude-2.1", - max_tokens: 1024, + const original = await originalClient.chat.completions.create({ + messages: [{ role: "user", content: `Say 'foo'` }], + temperature: 0, + seed: 42, + model: "gpt-3.5-turbo", }); - const patched = await patchedClient.messages.create({ - messages: [ - { - role: "user", - content: `What is 1 + 1? Respond only with "2" and nothing else.`, - }, - ], - model: "claude-2.1", - max_tokens: 1024, + const patched = await patchedClient.chat.completions.create({ + messages: [{ role: "user", content: `Say 'foo'` }], + temperature: 0, + seed: 42, + model: "gpt-3.5-turbo", }); - expect(patched.content).toEqual(original.content); + expect(patched.choices).toEqual(original.choices); // stream - const originalStream = await originalClient.messages.create({ - messages: [ - { - role: "user", - content: `What is 1 + 1? Respond only with "2" and nothing else.`, - }, - ], - model: "claude-2.1", - max_tokens: 1024, + const originalStream = await originalClient.chat.completions.create({ + messages: [{ role: "user", content: `Say 'foo'` }], + temperature: 0, + seed: 42, + model: "gpt-3.5-turbo", stream: true, }); - const originalChunks = []; + const originalChoices = []; for await (const chunk of originalStream) { - if (chunk.type === "message_delta") { - originalChunks.push(chunk.delta); - } + originalChoices.push(chunk.choices); } - const patchedStream = await patchedClient.messages.create({ - messages: [ - { - role: "user", - content: `What is 1 + 1? Respond only with "2" and nothing else.`, - }, - ], - model: "claude-2.1", - max_tokens: 1024, + const patchedStream = await patchedClient.chat.completions.create({ + messages: [{ role: "user", content: `Say 'foo'` }], + temperature: 0, + seed: 42, + model: "gpt-3.5-turbo", stream: true, }); - const patchedChunks = []; + const patchedChoices = []; for await (const chunk of patchedStream) { - if (chunk.type === "message_delta") { - patchedChunks.push(chunk.delta); - } + patchedChoices.push(chunk.choices); } - expect(patchedChunks).toEqual(originalChunks); + expect(patchedChoices).toEqual(originalChoices); for (const call of callSpy.mock.calls) { // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((call[2] as any)["method"]).toBe("POST"); diff --git a/js/yarn.lock b/js/yarn.lock index d61693513..829a88720 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -10,21 +10,6 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@anthropic-ai/sdk@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.16.1.tgz#7472c42389d9a5323c20afa53995e1c3b922b95d" - integrity sha512-vHgvfWEyFy5ktqam56Nrhv8MVa7EJthsRYNi+1OrFFfyrj9tR2/aji1QbVbQjYU/pPhPFaYrdCEC/MLPFrmKwA== - dependencies: - "@types/node" "^18.11.18" - "@types/node-fetch" "^2.6.4" - abort-controller "^3.0.0" - agentkeepalive "^4.2.1" - digest-fetch "^1.3.0" - form-data-encoder "1.7.2" - formdata-node "^4.3.2" - node-fetch "^2.6.7" - web-streams-polyfill "^3.2.1" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.21.4": version "7.21.4" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz"