@langchain/openai 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,10 @@ const outputs_1 = require("@langchain/core/outputs");
7
7
  const env_1 = require("@langchain/core/utils/env");
8
8
  const chat_models_1 = require("@langchain/core/language_models/chat_models");
9
9
  const function_calling_1 = require("@langchain/core/utils/function_calling");
10
+ const runnables_1 = require("@langchain/core/runnables");
11
+ const output_parsers_1 = require("@langchain/core/output_parsers");
12
+ const openai_tools_1 = require("@langchain/core/output_parsers/openai_tools");
13
+ const zod_to_json_schema_1 = require("zod-to-json-schema");
10
14
  const azure_js_1 = require("./utils/azure.cjs");
11
15
  const openai_js_1 = require("./utils/openai.cjs");
12
16
  const openai_format_fndef_js_1 = require("./utils/openai-format-fndef.cjs");
@@ -714,5 +718,91 @@ class ChatOpenAI extends chat_models_1.BaseChatModel {
714
718
  },
715
719
  });
716
720
  }
721
+ withStructuredOutput({ schema, name, method, includeRaw, }) {
722
+ let llm;
723
+ let outputParser;
724
+ if (method === "jsonMode") {
725
+ llm = this.bind({
726
+ response_format: { type: "json_object" },
727
+ });
728
+ outputParser = new output_parsers_1.JsonOutputParser();
729
+ }
730
+ else {
731
+ const functionName = name ?? "extract";
732
+ // Is function calling
733
+ if (isZodSchema(schema)) {
734
+ const asZodSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema);
735
+ llm = this.bind({
736
+ tools: [
737
+ {
738
+ type: "function",
739
+ function: {
740
+ name,
741
+ description: asZodSchema.description,
742
+ parameters: asZodSchema,
743
+ },
744
+ },
745
+ ],
746
+ tool_choice: {
747
+ type: "function",
748
+ function: {
749
+ name: functionName,
750
+ },
751
+ },
752
+ });
753
+ outputParser = new openai_tools_1.JsonOutputKeyToolsParser({
754
+ returnSingle: true,
755
+ keyName: functionName,
756
+ });
757
+ }
758
+ else {
759
+ llm = this.bind({
760
+ tools: [
761
+ {
762
+ type: "function",
763
+ function: {
764
+ name,
765
+ description: schema.description,
766
+ parameters: schema,
767
+ },
768
+ },
769
+ ],
770
+ tool_choice: {
771
+ type: "function",
772
+ function: {
773
+ name,
774
+ },
775
+ },
776
+ });
777
+ outputParser = new openai_tools_1.JsonOutputKeyToolsParser({
778
+ returnSingle: true,
779
+ keyName: functionName,
780
+ });
781
+ }
782
+ }
783
+ if (!includeRaw) {
784
+ return llm.pipe(outputParser);
785
+ }
786
+ const parserAssign = runnables_1.RunnablePassthrough.assign({
787
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
788
+ parsed: (input, config) => outputParser.invoke(input.raw, config),
789
+ });
790
+ const parserNone = runnables_1.RunnablePassthrough.assign({
791
+ parsed: () => null,
792
+ });
793
+ const parsedWithFallback = parserAssign.withFallbacks({
794
+ fallbacks: [parserNone],
795
+ });
796
+ return runnables_1.RunnableSequence.from([
797
+ {
798
+ raw: llm,
799
+ },
800
+ parsedWithFallback,
801
+ ]);
802
+ }
717
803
  }
718
804
  exports.ChatOpenAI = ChatOpenAI;
805
+ function isZodSchema(input) {
806
+ // Check for a characteristic method of Zod schemas
807
+ return typeof input?.parse === "function";
808
+ }
@@ -2,9 +2,10 @@ import { type ClientOptions, OpenAI as OpenAIClient } from "openai";
2
2
  import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
3
3
  import { type BaseMessage } from "@langchain/core/messages";
4
4
  import { ChatGenerationChunk, type ChatResult } from "@langchain/core/outputs";
5
- import type { StructuredToolInterface } from "@langchain/core/tools";
5
+ import { type StructuredToolInterface } from "@langchain/core/tools";
6
6
  import { BaseChatModel, type BaseChatModelParams } from "@langchain/core/language_models/chat_models";
7
- import type { BaseFunctionCallOptions } from "@langchain/core/language_models/base";
7
+ import type { BaseFunctionCallOptions, BaseLanguageModelInput, StructuredOutputMethodParams } from "@langchain/core/language_models/base";
8
+ import { Runnable } from "@langchain/core/runnables";
8
9
  import type { AzureOpenAIInput, OpenAICallOptions, OpenAIChatInput, OpenAICoreRequestOptions, LegacyOpenAIInput } from "./types.js";
9
10
  export type { AzureOpenAIInput, OpenAICallOptions, OpenAIChatInput };
10
11
  interface TokenUsage {
@@ -140,4 +141,9 @@ export declare class ChatOpenAI<CallOptions extends ChatOpenAICallOptions = Chat
140
141
  _llmType(): string;
141
142
  /** @ignore */
142
143
  _combineLLMOutput(...llmOutputs: OpenAILLMOutput[]): OpenAILLMOutput;
144
+ withStructuredOutput<RunOutput extends Record<string, any> = Record<string, any>>({ schema, name, method, includeRaw, }: StructuredOutputMethodParams<RunOutput, false>): Runnable<BaseLanguageModelInput, RunOutput>;
145
+ withStructuredOutput<RunOutput extends Record<string, any> = Record<string, any>>({ schema, name, method, includeRaw, }: StructuredOutputMethodParams<RunOutput, true>): Runnable<BaseLanguageModelInput, {
146
+ raw: BaseMessage;
147
+ parsed: RunOutput;
148
+ }>;
143
149
  }
@@ -4,6 +4,10 @@ import { ChatGenerationChunk, } from "@langchain/core/outputs";
4
4
  import { getEnvironmentVariable } from "@langchain/core/utils/env";
5
5
  import { BaseChatModel, } from "@langchain/core/language_models/chat_models";
6
6
  import { convertToOpenAITool } from "@langchain/core/utils/function_calling";
7
+ import { RunnablePassthrough, RunnableSequence, } from "@langchain/core/runnables";
8
+ import { JsonOutputParser } from "@langchain/core/output_parsers";
9
+ import { JsonOutputKeyToolsParser } from "@langchain/core/output_parsers/openai_tools";
10
+ import { zodToJsonSchema } from "zod-to-json-schema";
7
11
  import { getEndpoint } from "./utils/azure.js";
8
12
  import { wrapOpenAIClientError } from "./utils/openai.js";
9
13
  import { formatFunctionDefinitions, } from "./utils/openai-format-fndef.js";
@@ -710,4 +714,90 @@ export class ChatOpenAI extends BaseChatModel {
710
714
  },
711
715
  });
712
716
  }
717
+ withStructuredOutput({ schema, name, method, includeRaw, }) {
718
+ let llm;
719
+ let outputParser;
720
+ if (method === "jsonMode") {
721
+ llm = this.bind({
722
+ response_format: { type: "json_object" },
723
+ });
724
+ outputParser = new JsonOutputParser();
725
+ }
726
+ else {
727
+ const functionName = name ?? "extract";
728
+ // Is function calling
729
+ if (isZodSchema(schema)) {
730
+ const asZodSchema = zodToJsonSchema(schema);
731
+ llm = this.bind({
732
+ tools: [
733
+ {
734
+ type: "function",
735
+ function: {
736
+ name,
737
+ description: asZodSchema.description,
738
+ parameters: asZodSchema,
739
+ },
740
+ },
741
+ ],
742
+ tool_choice: {
743
+ type: "function",
744
+ function: {
745
+ name: functionName,
746
+ },
747
+ },
748
+ });
749
+ outputParser = new JsonOutputKeyToolsParser({
750
+ returnSingle: true,
751
+ keyName: functionName,
752
+ });
753
+ }
754
+ else {
755
+ llm = this.bind({
756
+ tools: [
757
+ {
758
+ type: "function",
759
+ function: {
760
+ name,
761
+ description: schema.description,
762
+ parameters: schema,
763
+ },
764
+ },
765
+ ],
766
+ tool_choice: {
767
+ type: "function",
768
+ function: {
769
+ name,
770
+ },
771
+ },
772
+ });
773
+ outputParser = new JsonOutputKeyToolsParser({
774
+ returnSingle: true,
775
+ keyName: functionName,
776
+ });
777
+ }
778
+ }
779
+ if (!includeRaw) {
780
+ return llm.pipe(outputParser);
781
+ }
782
+ const parserAssign = RunnablePassthrough.assign({
783
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
784
+ parsed: (input, config) => outputParser.invoke(input.raw, config),
785
+ });
786
+ const parserNone = RunnablePassthrough.assign({
787
+ parsed: () => null,
788
+ });
789
+ const parsedWithFallback = parserAssign.withFallbacks({
790
+ fallbacks: [parserNone],
791
+ });
792
+ return RunnableSequence.from([
793
+ {
794
+ raw: llm,
795
+ },
796
+ parsedWithFallback,
797
+ ]);
798
+ }
799
+ }
800
+ function isZodSchema(input) {
801
+ // Check for a characteristic method of Zod schemas
802
+ return typeof input?.parse === "function";
713
803
  }
@@ -1,9 +1,11 @@
1
- import { test, jest, expect } from "@jest/globals";
1
+ import { test, jest, expect, describe } from "@jest/globals";
2
2
  import { ChatMessage, HumanMessage, SystemMessage, } from "@langchain/core/messages";
3
3
  import { ChatPromptValue } from "@langchain/core/prompt_values";
4
4
  import { PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate, } from "@langchain/core/prompts";
5
5
  import { CallbackManager } from "@langchain/core/callbacks/manager";
6
6
  import { InMemoryCache } from "@langchain/core/caches";
7
+ import { z } from "zod";
8
+ import { zodToJsonSchema } from "zod-to-json-schema";
7
9
  import { ChatOpenAI } from "../chat_models.js";
8
10
  test("Test ChatOpenAI", async () => {
9
11
  const chat = new ChatOpenAI({ modelName: "gpt-3.5-turbo", maxTokens: 10 });
@@ -628,3 +630,173 @@ test("Test ChatOpenAI token usage reporting for streaming calls", async () => {
628
630
  expect(streamingTokenUsed).toEqual(nonStreamingTokenUsed);
629
631
  }
630
632
  });
633
+ describe("ChatOpenAI withStructuredOutput", () => {
634
+ test("withStructuredOutput zod schema function calling", async () => {
635
+ const model = new ChatOpenAI({
636
+ temperature: 0,
637
+ modelName: "gpt-4-turbo-preview",
638
+ });
639
+ const calculatorSchema = z.object({
640
+ operation: z.enum(["add", "subtract", "multiply", "divide"]),
641
+ number1: z.number(),
642
+ number2: z.number(),
643
+ });
644
+ const modelWithStructuredOutput = model.withStructuredOutput({
645
+ schema: calculatorSchema,
646
+ name: "calculator",
647
+ });
648
+ const prompt = ChatPromptTemplate.fromMessages([
649
+ "system",
650
+ "You are VERY bad at math and must always use a calculator.",
651
+ "human",
652
+ "Please help me!! What is 2 + 2?",
653
+ ]);
654
+ const chain = prompt.pipe(modelWithStructuredOutput);
655
+ const result = await chain.invoke({});
656
+ console.log(result);
657
+ expect("operation" in result).toBe(true);
658
+ expect("number1" in result).toBe(true);
659
+ expect("number2" in result).toBe(true);
660
+ });
661
+ test("withStructuredOutput zod schema JSON mode", async () => {
662
+ const model = new ChatOpenAI({
663
+ temperature: 0,
664
+ modelName: "gpt-4-turbo-preview",
665
+ });
666
+ const calculatorSchema = z.object({
667
+ operation: z.enum(["add", "subtract", "multiply", "divide"]),
668
+ number1: z.number(),
669
+ number2: z.number(),
670
+ });
671
+ const modelWithStructuredOutput = model.withStructuredOutput({
672
+ schema: calculatorSchema,
673
+ name: "calculator",
674
+ method: "jsonMode",
675
+ });
676
+ const prompt = ChatPromptTemplate.fromMessages([
677
+ "system",
678
+ `You are VERY bad at math and must always use a calculator.
679
+ Respond with a JSON object containing three keys:
680
+ 'operation': the type of operation to execute, either 'add', 'subtract', 'multiply' or 'divide',
681
+ 'number1': the first number to operate on,
682
+ 'number2': the second number to operate on.
683
+ `,
684
+ "human",
685
+ "Please help me!! What is 2 + 2?",
686
+ ]);
687
+ const chain = prompt.pipe(modelWithStructuredOutput);
688
+ const result = await chain.invoke({});
689
+ console.log(result);
690
+ expect("operation" in result).toBe(true);
691
+ expect("number1" in result).toBe(true);
692
+ expect("number2" in result).toBe(true);
693
+ });
694
+ test("withStructuredOutput JSON schema function calling", async () => {
695
+ const model = new ChatOpenAI({
696
+ temperature: 0,
697
+ modelName: "gpt-4-turbo-preview",
698
+ });
699
+ const calculatorSchema = z.object({
700
+ operation: z.enum(["add", "subtract", "multiply", "divide"]),
701
+ number1: z.number(),
702
+ number2: z.number(),
703
+ });
704
+ const modelWithStructuredOutput = model.withStructuredOutput({
705
+ schema: zodToJsonSchema(calculatorSchema),
706
+ name: "calculator",
707
+ });
708
+ const prompt = ChatPromptTemplate.fromMessages([
709
+ "system",
710
+ `You are VERY bad at math and must always use a calculator.`,
711
+ "human",
712
+ "Please help me!! What is 2 + 2?",
713
+ ]);
714
+ const chain = prompt.pipe(modelWithStructuredOutput);
715
+ const result = await chain.invoke({});
716
+ console.log(result);
717
+ expect("operation" in result).toBe(true);
718
+ expect("number1" in result).toBe(true);
719
+ expect("number2" in result).toBe(true);
720
+ });
721
+ test("withStructuredOutput JSON schema JSON mode", async () => {
722
+ const model = new ChatOpenAI({
723
+ temperature: 0,
724
+ modelName: "gpt-4-turbo-preview",
725
+ });
726
+ const calculatorSchema = z.object({
727
+ operation: z.enum(["add", "subtract", "multiply", "divide"]),
728
+ number1: z.number(),
729
+ number2: z.number(),
730
+ });
731
+ const modelWithStructuredOutput = model.withStructuredOutput({
732
+ schema: zodToJsonSchema(calculatorSchema),
733
+ name: "calculator",
734
+ method: "jsonMode",
735
+ });
736
+ const prompt = ChatPromptTemplate.fromMessages([
737
+ "system",
738
+ `You are VERY bad at math and must always use a calculator.
739
+ Respond with a JSON object containing three keys:
740
+ 'operation': the type of operation to execute, either 'add', 'subtract', 'multiply' or 'divide',
741
+ 'number1': the first number to operate on,
742
+ 'number2': the second number to operate on.
743
+ `,
744
+ "human",
745
+ "Please help me!! What is 2 + 2?",
746
+ ]);
747
+ const chain = prompt.pipe(modelWithStructuredOutput);
748
+ const result = await chain.invoke({});
749
+ console.log(result);
750
+ expect("operation" in result).toBe(true);
751
+ expect("number1" in result).toBe(true);
752
+ expect("number2" in result).toBe(true);
753
+ });
754
+ test("withStructuredOutput includeRaw true", async () => {
755
+ const model = new ChatOpenAI({
756
+ temperature: 0,
757
+ modelName: "gpt-4-turbo-preview",
758
+ });
759
+ const calculatorSchema = z.object({
760
+ operation: z.enum(["add", "subtract", "multiply", "divide"]),
761
+ number1: z.number(),
762
+ number2: z.number(),
763
+ });
764
+ const modelWithStructuredOutput = model.withStructuredOutput({
765
+ schema: calculatorSchema,
766
+ name: "calculator",
767
+ includeRaw: true,
768
+ });
769
+ const prompt = ChatPromptTemplate.fromMessages([
770
+ "system",
771
+ "You are VERY bad at math and must always use a calculator.",
772
+ "human",
773
+ "Please help me!! What is 2 + 2?",
774
+ ]);
775
+ const chain = prompt.pipe(modelWithStructuredOutput);
776
+ const result = await chain.invoke({});
777
+ console.log(result);
778
+ expect("parsed" in result).toBe(true);
779
+ // Need to make TS happy :)
780
+ if (!("parsed" in result)) {
781
+ throw new Error("parsed not in result");
782
+ }
783
+ const { parsed } = result;
784
+ expect("operation" in parsed).toBe(true);
785
+ expect("number1" in parsed).toBe(true);
786
+ expect("number2" in parsed).toBe(true);
787
+ expect("raw" in result).toBe(true);
788
+ // Need to make TS happy :)
789
+ if (!("raw" in result)) {
790
+ throw new Error("raw not in result");
791
+ }
792
+ const { raw } = result;
793
+ expect(raw.additional_kwargs.tool_calls?.length).toBeGreaterThan(0);
794
+ expect(raw.additional_kwargs.tool_calls?.[0].function.name).toBe("calculator");
795
+ expect("operation" in
796
+ JSON.parse(raw.additional_kwargs.tool_calls?.[0].function.arguments ?? "")).toBe(true);
797
+ expect("number1" in
798
+ JSON.parse(raw.additional_kwargs.tool_calls?.[0].function.arguments ?? "")).toBe(true);
799
+ expect("number2" in
800
+ JSON.parse(raw.additional_kwargs.tool_calls?.[0].function.arguments ?? "")).toBe(true);
801
+ });
802
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/openai",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "OpenAI integrations for LangChain.js",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,6 +12,7 @@
12
12
  "type": "git",
13
13
  "url": "git@github.com:langchain-ai/langchainjs.git"
14
14
  },
15
+ "homepage": "https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-openai/",
15
16
  "scripts": {
16
17
  "build": "yarn run build:deps && yarn clean && yarn build:esm && yarn build:cjs && yarn build:scripts",
17
18
  "build:deps": "yarn run turbo:command build --filter=@langchain/core",
@@ -23,7 +24,7 @@
23
24
  "lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/*.ts src/**/*.ts",
24
25
  "lint": "yarn lint:eslint && yarn lint:dpdm",
25
26
  "lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm",
26
- "clean": "rimraf .turbo/ dist/ && NODE_OPTIONS=--max-old-space-size=4096 yarn create-entrypoints -- --pre",
27
+ "clean": "rimraf .turbo/ dist/ && NODE_OPTIONS=--max-old-space-size=4096 yarn lc-build --config ./langchain.config.js --create-entrypoints --pre",
27
28
  "prepack": "yarn build",
28
29
  "test": "yarn run build:deps && NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts --testTimeout 30000 --maxWorkers=50%",
29
30
  "test:watch": "yarn run build:deps && NODE_OPTIONS=--experimental-vm-modules jest --watch --testPathIgnorePatterns=\\.int\\.test.ts",
@@ -38,7 +39,7 @@
38
39
  "author": "LangChain",
39
40
  "license": "MIT",
40
41
  "dependencies": {
41
- "@langchain/core": "~0.1.13",
42
+ "@langchain/core": "~0.1.41",
42
43
  "js-tiktoken": "^1.0.7",
43
44
  "openai": "^4.26.0",
44
45
  "zod": "^3.22.4",