From 6b7d2a54d0a52b2ef38d0570f7cfc4fc30d872a4 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 8 Feb 2025 14:13:28 -0800 Subject: [PATCH 1/4] Add final overload to withStructuredOutput --- .../src/language_models/chat_models.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index 206e8b01dc6a..46adab26ff3d 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -890,6 +890,25 @@ export abstract class BaseChatModel< config?: StructuredOutputMethodOptions ): Runnable; + withStructuredOutput< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends Record = Record + >( + outputSchema: + | z.ZodType + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Record, + config?: StructuredOutputMethodOptions + ): + | Runnable + | Runnable< + BaseLanguageModelInput, + { + raw: BaseMessage; + parsed: RunOutput; + } + >; + withStructuredOutput< // eslint-disable-next-line @typescript-eslint/no-explicit-any RunOutput extends Record = Record From af86324b0f08d49d49b5f4f0b941119bdb30f90f Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 8 Feb 2025 14:14:39 -0800 Subject: [PATCH 2/4] Make default Ollama withStructuredOutput use built-in structured output format --- libs/langchain-ollama/package.json | 10 +- libs/langchain-ollama/src/chat_models.ts | 128 +++++++++++++++++- ...chat_models_structured_output.int.test.ts} | 17 +++ yarn.lock | 23 +++- 4 files changed, 161 insertions(+), 17 deletions(-) rename libs/langchain-ollama/src/tests/{chat_models-tools.int.test.ts => chat_models_structured_output.int.test.ts} (85%) diff --git a/libs/langchain-ollama/package.json b/libs/langchain-ollama/package.json index 389575ae1ed5..84501e325cda 100644 --- a/libs/langchain-ollama/package.json +++ b/libs/langchain-ollama/package.json @@ -32,8 +32,10 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "ollama": "^0.5.9", - "uuid": "^10.0.0" + "ollama": "^0.5.12", + "uuid": "^10.0.0", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" @@ -62,9 +64,7 @@ "release-it": "^17.6.0", "rollup": "^4.5.2", "ts-jest": "^29.1.0", - "typescript": "<5.2.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.23.0" + "typescript": "<5.2.0" }, "publishConfig": { "access": "public" diff --git a/libs/langchain-ollama/src/chat_models.ts b/libs/langchain-ollama/src/chat_models.ts index 1eeb745f5bba..50a7fc86d448 100644 --- a/libs/langchain-ollama/src/chat_models.ts +++ b/libs/langchain-ollama/src/chat_models.ts @@ -3,7 +3,10 @@ import { UsageMetadata, type BaseMessage, } from "@langchain/core/messages"; -import { BaseLanguageModelInput } from "@langchain/core/language_models/base"; +import { + BaseLanguageModelInput, + StructuredOutputMethodOptions, +} from "@langchain/core/language_models/base"; import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"; import { type BaseChatModelParams, @@ -21,14 +24,23 @@ import type { Message as OllamaMessage, Tool as OllamaTool, } from "ollama"; -import { Runnable } from "@langchain/core/runnables"; +import { + Runnable, + RunnablePassthrough, + RunnableSequence, +} from "@langchain/core/runnables"; import { convertToOpenAITool } from "@langchain/core/utils/function_calling"; import { concat } from "@langchain/core/utils/stream"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { convertOllamaMessagesToLangChain, convertToOllamaMessages, } from "./utils.js"; import { OllamaCamelCaseOptions } from "./types.js"; +import { isZodSchema } from "@langchain/core/utils/types"; +import { JsonOutputParser } from "@langchain/core/output_parsers"; +import { StructuredOutputParser } from "@langchain/core/output_parsers"; export interface ChatOllamaCallOptions extends BaseChatModelCallOptions { /** @@ -36,6 +48,8 @@ export interface ChatOllamaCallOptions extends BaseChatModelCallOptions { */ stop?: string[]; tools?: BindToolsInput[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + format?: string | Record; } export interface PullModelOptions { @@ -82,7 +96,8 @@ export interface ChatOllamaInput */ checkOrPullModel?: boolean; streaming?: boolean; - format?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + format?: string | Record; } /** @@ -453,7 +468,8 @@ export class ChatOllama streaming?: boolean; - format?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + format?: string | Record; keepAlive?: string | number; @@ -575,7 +591,7 @@ export class ChatOllama return { model: this.model, - format: this.format, + format: options?.format ?? this.format, keep_alive: this.keepAlive, options: { numa: this.numa, @@ -763,4 +779,106 @@ export class ChatOllama }), }); } + + withStructuredOutput< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends Record = Record + >( + outputSchema: + | z.ZodType + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Record, + config?: StructuredOutputMethodOptions + ): Runnable; + + withStructuredOutput< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends Record = Record + >( + outputSchema: + | z.ZodType + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Record, + config?: StructuredOutputMethodOptions + ): Runnable; + + withStructuredOutput< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends Record = Record + >( + outputSchema: + | z.ZodType + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Record, + config?: StructuredOutputMethodOptions + ): + | Runnable + | Runnable< + BaseLanguageModelInput, + { + raw: BaseMessage; + parsed: RunOutput; + } + >; + + withStructuredOutput< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends Record = Record + >( + outputSchema: + | z.ZodType + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Record, + config?: StructuredOutputMethodOptions + ): + | Runnable + | Runnable< + BaseLanguageModelInput, + { + raw: BaseMessage; + parsed: RunOutput; + } + > { + if (config?.method === undefined || config?.method === "jsonSchema") { + const outputSchemaIsZod = isZodSchema(outputSchema); + const jsonSchema = outputSchemaIsZod + ? zodToJsonSchema(outputSchema) + : outputSchema; + const llm = this.bind({ + format: jsonSchema, + }); + const outputParser = outputSchemaIsZod + ? StructuredOutputParser.fromZodSchema(outputSchema) + : new JsonOutputParser(); + + if (!config?.includeRaw) { + return llm.pipe(outputParser) as Runnable< + BaseLanguageModelInput, + RunOutput + >; + } + + const parserAssign = RunnablePassthrough.assign({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parsed: (input: any, config) => outputParser.invoke(input.raw, config), + }); + const parserNone = RunnablePassthrough.assign({ + parsed: () => null, + }); + const parsedWithFallback = parserAssign.withFallbacks({ + fallbacks: [parserNone], + }); + return RunnableSequence.from< + BaseLanguageModelInput, + { raw: BaseMessage; parsed: RunOutput } + >([ + { + raw: llm, + }, + parsedWithFallback, + ]); + } else { + return super.withStructuredOutput(outputSchema, config); + } + } } diff --git a/libs/langchain-ollama/src/tests/chat_models-tools.int.test.ts b/libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts similarity index 85% rename from libs/langchain-ollama/src/tests/chat_models-tools.int.test.ts rename to libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts index 0d699060eca0..e035f9e4758f 100644 --- a/libs/langchain-ollama/src/tests/chat_models-tools.int.test.ts +++ b/libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts @@ -98,6 +98,23 @@ test("Ollama can call withStructuredOutput includeRaw", async () => { includeRaw: true, }); + const result = await model.invoke(messageHistory); + expect(result).toBeDefined(); + expect(result.parsed.location).toBeDefined(); + expect(result.parsed.location).not.toBe(""); + expect((result.raw as AIMessage).tool_calls?.length).toBe(0); +}); + +test("Ollama can call withStructuredOutput includeRaw with tool calling", async () => { + const model = new ChatOllama({ + model: "llama3-groq-tool-use", + maxRetries: 1, + }).withStructuredOutput(weatherTool.schema, { + name: weatherTool.name, + includeRaw: true, + method: "functionCalling", + }); + const result = await model.invoke(messageHistory); expect(result).toBeDefined(); expect(result.parsed.location).toBeDefined(); diff --git a/yarn.lock b/yarn.lock index 8d8124231be9..71b247416ac4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12990,15 +12990,15 @@ __metadata: eslint-plugin-prettier: ^4.2.1 jest: ^29.5.0 jest-environment-node: ^29.6.4 - ollama: ^0.5.9 + ollama: ^0.5.12 prettier: ^2.8.3 release-it: ^17.6.0 rollup: ^4.5.2 ts-jest: ^29.1.0 typescript: <5.2.0 uuid: ^10.0.0 - zod: ^3.22.4 - zod-to-json-schema: ^3.23.0 + zod: ^3.24.1 + zod-to-json-schema: ^3.24.1 peerDependencies: "@langchain/core": ">=0.2.21 <0.4.0" languageName: unknown @@ -36216,12 +36216,12 @@ __metadata: languageName: node linkType: hard -"ollama@npm:^0.5.9": - version: 0.5.9 - resolution: "ollama@npm:0.5.9" +"ollama@npm:^0.5.12": + version: 0.5.12 + resolution: "ollama@npm:0.5.12" dependencies: whatwg-fetch: ^3.6.20 - checksum: bfaadcec6273d86fcc7c94e5e9e571a7b6b84b852b407a473f3bac7dc69b7b11815a163ae549b5318267a00f192d39696225309812319d2edc8a98a079ace475 + checksum: 0abc1151d2cfd02198829f706f8efca978c8562691e7502924166798f6a0cd7e1bf51e085d313ddf5a76507a36ffa12b48a66d4dd659b419474c2f33e3f03b44 languageName: node linkType: hard @@ -44942,6 +44942,15 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.24.1": + version: 3.24.1 + resolution: "zod-to-json-schema@npm:3.24.1" + peerDependencies: + zod: ^3.24.1 + checksum: 7195563f611bc21ea7f44129b8e32780125a9bd98b2e6b8709ac98bd2645729fecd87b8aeeaa8789617ee3f38e6585bab23dd613e2a35c31c6c157908f7a1681 + languageName: node + linkType: hard + "zod@npm:3.23.8": version: 3.23.8 resolution: "zod@npm:3.23.8" From 4ea3822628b71775e66f0078262f133eb47f2e47 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 8 Feb 2025 14:24:39 -0800 Subject: [PATCH 3/4] Fix build --- .../src/language_models/chat_models.ts | 19 ------------------- libs/langchain-ollama/src/chat_models.ts | 12 ++++++++---- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index 46adab26ff3d..206e8b01dc6a 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -890,25 +890,6 @@ export abstract class BaseChatModel< config?: StructuredOutputMethodOptions ): Runnable; - withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - RunOutput extends Record = Record - >( - outputSchema: - | z.ZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Record, - config?: StructuredOutputMethodOptions - ): - | Runnable - | Runnable< - BaseLanguageModelInput, - { - raw: BaseMessage; - parsed: RunOutput; - } - >; - withStructuredOutput< // eslint-disable-next-line @typescript-eslint/no-explicit-any RunOutput extends Record = Record diff --git a/libs/langchain-ollama/src/chat_models.ts b/libs/langchain-ollama/src/chat_models.ts index 50a7fc86d448..dc2b2f2a7761 100644 --- a/libs/langchain-ollama/src/chat_models.ts +++ b/libs/langchain-ollama/src/chat_models.ts @@ -31,6 +31,11 @@ import { } from "@langchain/core/runnables"; import { convertToOpenAITool } from "@langchain/core/utils/function_calling"; import { concat } from "@langchain/core/utils/stream"; +import { + JsonOutputParser, + StructuredOutputParser, +} from "@langchain/core/output_parsers"; +import { isZodSchema } from "@langchain/core/utils/types"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { @@ -38,9 +43,6 @@ import { convertToOllamaMessages, } from "./utils.js"; import { OllamaCamelCaseOptions } from "./types.js"; -import { isZodSchema } from "@langchain/core/utils/types"; -import { JsonOutputParser } from "@langchain/core/output_parsers"; -import { StructuredOutputParser } from "@langchain/core/output_parsers"; export interface ChatOllamaCallOptions extends BaseChatModelCallOptions { /** @@ -878,7 +880,9 @@ export class ChatOllama parsedWithFallback, ]); } else { - return super.withStructuredOutput(outputSchema, config); + // TODO: Fix this type in core + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return super.withStructuredOutput(outputSchema, config as any); } } } From c99d35d387b501a62da90c16a95f16bb86017330 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 8 Feb 2025 14:34:00 -0800 Subject: [PATCH 4/4] Make not default --- libs/langchain-ollama/src/chat_models.ts | 3 ++- .../src/tests/chat_models_structured_output.int.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/langchain-ollama/src/chat_models.ts b/libs/langchain-ollama/src/chat_models.ts index dc2b2f2a7761..fc23966a26a4 100644 --- a/libs/langchain-ollama/src/chat_models.ts +++ b/libs/langchain-ollama/src/chat_models.ts @@ -841,7 +841,8 @@ export class ChatOllama parsed: RunOutput; } > { - if (config?.method === undefined || config?.method === "jsonSchema") { + // TODO: Make this method the default in a minor bump + if (config?.method === "jsonSchema") { const outputSchemaIsZod = isZodSchema(outputSchema); const jsonSchema = outputSchemaIsZod ? zodToJsonSchema(outputSchema) diff --git a/libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts b/libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts index e035f9e4758f..cba16e4639d5 100644 --- a/libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts +++ b/libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts @@ -89,13 +89,14 @@ test("Ollama can call withStructuredOutput", async () => { expect(result.location).not.toBe(""); }); -test("Ollama can call withStructuredOutput includeRaw", async () => { +test("Ollama can call withStructuredOutput includeRaw JSON Schema", async () => { const model = new ChatOllama({ model: "llama3-groq-tool-use", maxRetries: 1, }).withStructuredOutput(weatherTool.schema, { name: weatherTool.name, includeRaw: true, + method: "jsonSchema", }); const result = await model.invoke(messageHistory); @@ -112,7 +113,6 @@ test("Ollama can call withStructuredOutput includeRaw with tool calling", async }).withStructuredOutput(weatherTool.schema, { name: weatherTool.name, includeRaw: true, - method: "functionCalling", }); const result = await model.invoke(messageHistory);