Skip to content

Commit 0050a55

Browse files
authored
feat(ollama): Add support for Ollama built-in JSON schema with withStructuredOutput (#7672)
1 parent 01e8614 commit 0050a55

File tree

4 files changed

+167
-18
lines changed

4 files changed

+167
-18
lines changed

libs/langchain-ollama/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
"author": "LangChain",
3333
"license": "MIT",
3434
"dependencies": {
35-
"ollama": "^0.5.9",
36-
"uuid": "^10.0.0"
35+
"ollama": "^0.5.12",
36+
"uuid": "^10.0.0",
37+
"zod": "^3.24.1",
38+
"zod-to-json-schema": "^3.24.1"
3739
},
3840
"peerDependencies": {
3941
"@langchain/core": ">=0.2.21 <0.4.0"
@@ -62,9 +64,7 @@
6264
"release-it": "^17.6.0",
6365
"rollup": "^4.5.2",
6466
"ts-jest": "^29.1.0",
65-
"typescript": "<5.2.0",
66-
"zod": "^3.22.4",
67-
"zod-to-json-schema": "^3.23.0"
67+
"typescript": "<5.2.0"
6868
},
6969
"publishConfig": {
7070
"access": "public"

libs/langchain-ollama/src/chat_models.ts

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import {
33
UsageMetadata,
44
type BaseMessage,
55
} from "@langchain/core/messages";
6-
import { BaseLanguageModelInput } from "@langchain/core/language_models/base";
6+
import {
7+
BaseLanguageModelInput,
8+
StructuredOutputMethodOptions,
9+
} from "@langchain/core/language_models/base";
710
import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
811
import {
912
type BaseChatModelParams,
@@ -21,9 +24,20 @@ import type {
2124
Message as OllamaMessage,
2225
Tool as OllamaTool,
2326
} from "ollama";
24-
import { Runnable } from "@langchain/core/runnables";
27+
import {
28+
Runnable,
29+
RunnablePassthrough,
30+
RunnableSequence,
31+
} from "@langchain/core/runnables";
2532
import { convertToOpenAITool } from "@langchain/core/utils/function_calling";
2633
import { concat } from "@langchain/core/utils/stream";
34+
import {
35+
JsonOutputParser,
36+
StructuredOutputParser,
37+
} from "@langchain/core/output_parsers";
38+
import { isZodSchema } from "@langchain/core/utils/types";
39+
import { z } from "zod";
40+
import { zodToJsonSchema } from "zod-to-json-schema";
2741
import {
2842
convertOllamaMessagesToLangChain,
2943
convertToOllamaMessages,
@@ -36,6 +50,8 @@ export interface ChatOllamaCallOptions extends BaseChatModelCallOptions {
3650
*/
3751
stop?: string[];
3852
tools?: BindToolsInput[];
53+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54+
format?: string | Record<string, any>;
3955
}
4056

4157
export interface PullModelOptions {
@@ -82,7 +98,8 @@ export interface ChatOllamaInput
8298
*/
8399
checkOrPullModel?: boolean;
84100
streaming?: boolean;
85-
format?: string;
101+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102+
format?: string | Record<string, any>;
86103
}
87104

88105
/**
@@ -453,7 +470,8 @@ export class ChatOllama
453470

454471
streaming?: boolean;
455472

456-
format?: string;
473+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
474+
format?: string | Record<string, any>;
457475

458476
keepAlive?: string | number;
459477

@@ -575,7 +593,7 @@ export class ChatOllama
575593

576594
return {
577595
model: this.model,
578-
format: this.format,
596+
format: options?.format ?? this.format,
579597
keep_alive: this.keepAlive,
580598
options: {
581599
numa: this.numa,
@@ -763,4 +781,109 @@ export class ChatOllama
763781
}),
764782
});
765783
}
784+
785+
withStructuredOutput<
786+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
787+
RunOutput extends Record<string, any> = Record<string, any>
788+
>(
789+
outputSchema:
790+
| z.ZodType<RunOutput>
791+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
792+
| Record<string, any>,
793+
config?: StructuredOutputMethodOptions<false>
794+
): Runnable<BaseLanguageModelInput, RunOutput>;
795+
796+
withStructuredOutput<
797+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
798+
RunOutput extends Record<string, any> = Record<string, any>
799+
>(
800+
outputSchema:
801+
| z.ZodType<RunOutput>
802+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
803+
| Record<string, any>,
804+
config?: StructuredOutputMethodOptions<true>
805+
): Runnable<BaseLanguageModelInput, { raw: BaseMessage; parsed: RunOutput }>;
806+
807+
withStructuredOutput<
808+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
809+
RunOutput extends Record<string, any> = Record<string, any>
810+
>(
811+
outputSchema:
812+
| z.ZodType<RunOutput>
813+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
814+
| Record<string, any>,
815+
config?: StructuredOutputMethodOptions<boolean>
816+
):
817+
| Runnable<BaseLanguageModelInput, RunOutput>
818+
| Runnable<
819+
BaseLanguageModelInput,
820+
{
821+
raw: BaseMessage;
822+
parsed: RunOutput;
823+
}
824+
>;
825+
826+
withStructuredOutput<
827+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
828+
RunOutput extends Record<string, any> = Record<string, any>
829+
>(
830+
outputSchema:
831+
| z.ZodType<RunOutput>
832+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
833+
| Record<string, any>,
834+
config?: StructuredOutputMethodOptions<boolean>
835+
):
836+
| Runnable<BaseLanguageModelInput, RunOutput>
837+
| Runnable<
838+
BaseLanguageModelInput,
839+
{
840+
raw: BaseMessage;
841+
parsed: RunOutput;
842+
}
843+
> {
844+
// TODO: Make this method the default in a minor bump
845+
if (config?.method === "jsonSchema") {
846+
const outputSchemaIsZod = isZodSchema(outputSchema);
847+
const jsonSchema = outputSchemaIsZod
848+
? zodToJsonSchema(outputSchema)
849+
: outputSchema;
850+
const llm = this.bind({
851+
format: jsonSchema,
852+
});
853+
const outputParser = outputSchemaIsZod
854+
? StructuredOutputParser.fromZodSchema(outputSchema)
855+
: new JsonOutputParser<RunOutput>();
856+
857+
if (!config?.includeRaw) {
858+
return llm.pipe(outputParser) as Runnable<
859+
BaseLanguageModelInput,
860+
RunOutput
861+
>;
862+
}
863+
864+
const parserAssign = RunnablePassthrough.assign({
865+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
866+
parsed: (input: any, config) => outputParser.invoke(input.raw, config),
867+
});
868+
const parserNone = RunnablePassthrough.assign({
869+
parsed: () => null,
870+
});
871+
const parsedWithFallback = parserAssign.withFallbacks({
872+
fallbacks: [parserNone],
873+
});
874+
return RunnableSequence.from<
875+
BaseLanguageModelInput,
876+
{ raw: BaseMessage; parsed: RunOutput }
877+
>([
878+
{
879+
raw: llm,
880+
},
881+
parsedWithFallback,
882+
]);
883+
} else {
884+
// TODO: Fix this type in core
885+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
886+
return super.withStructuredOutput<RunOutput>(outputSchema, config as any);
887+
}
888+
}
766889
}

libs/langchain-ollama/src/tests/chat_models-tools.int.test.ts renamed to libs/langchain-ollama/src/tests/chat_models_structured_output.int.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,24 @@ test("Ollama can call withStructuredOutput", async () => {
8989
expect(result.location).not.toBe("");
9090
});
9191

92-
test("Ollama can call withStructuredOutput includeRaw", async () => {
92+
test("Ollama can call withStructuredOutput includeRaw JSON Schema", async () => {
93+
const model = new ChatOllama({
94+
model: "llama3-groq-tool-use",
95+
maxRetries: 1,
96+
}).withStructuredOutput(weatherTool.schema, {
97+
name: weatherTool.name,
98+
includeRaw: true,
99+
method: "jsonSchema",
100+
});
101+
102+
const result = await model.invoke(messageHistory);
103+
expect(result).toBeDefined();
104+
expect(result.parsed.location).toBeDefined();
105+
expect(result.parsed.location).not.toBe("");
106+
expect((result.raw as AIMessage).tool_calls?.length).toBe(0);
107+
});
108+
109+
test("Ollama can call withStructuredOutput includeRaw with tool calling", async () => {
93110
const model = new ChatOllama({
94111
model: "llama3-groq-tool-use",
95112
maxRetries: 1,

yarn.lock

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12990,15 +12990,15 @@ __metadata:
1299012990
eslint-plugin-prettier: ^4.2.1
1299112991
jest: ^29.5.0
1299212992
jest-environment-node: ^29.6.4
12993-
ollama: ^0.5.9
12993+
ollama: ^0.5.12
1299412994
prettier: ^2.8.3
1299512995
release-it: ^17.6.0
1299612996
rollup: ^4.5.2
1299712997
ts-jest: ^29.1.0
1299812998
typescript: <5.2.0
1299912999
uuid: ^10.0.0
13000-
zod: ^3.22.4
13001-
zod-to-json-schema: ^3.23.0
13000+
zod: ^3.24.1
13001+
zod-to-json-schema: ^3.24.1
1300213002
peerDependencies:
1300313003
"@langchain/core": ">=0.2.21 <0.4.0"
1300413004
languageName: unknown
@@ -36216,12 +36216,12 @@ __metadata:
3621636216
languageName: node
3621736217
linkType: hard
3621836218

36219-
"ollama@npm:^0.5.9":
36220-
version: 0.5.9
36221-
resolution: "ollama@npm:0.5.9"
36219+
"ollama@npm:^0.5.12":
36220+
version: 0.5.12
36221+
resolution: "ollama@npm:0.5.12"
3622236222
dependencies:
3622336223
whatwg-fetch: ^3.6.20
36224-
checksum: bfaadcec6273d86fcc7c94e5e9e571a7b6b84b852b407a473f3bac7dc69b7b11815a163ae549b5318267a00f192d39696225309812319d2edc8a98a079ace475
36224+
checksum: 0abc1151d2cfd02198829f706f8efca978c8562691e7502924166798f6a0cd7e1bf51e085d313ddf5a76507a36ffa12b48a66d4dd659b419474c2f33e3f03b44
3622536225
languageName: node
3622636226
linkType: hard
3622736227

@@ -44942,6 +44942,15 @@ __metadata:
4494244942
languageName: node
4494344943
linkType: hard
4494444944

44945+
"zod-to-json-schema@npm:^3.24.1":
44946+
version: 3.24.1
44947+
resolution: "zod-to-json-schema@npm:3.24.1"
44948+
peerDependencies:
44949+
zod: ^3.24.1
44950+
checksum: 7195563f611bc21ea7f44129b8e32780125a9bd98b2e6b8709ac98bd2645729fecd87b8aeeaa8789617ee3f38e6585bab23dd613e2a35c31c6c157908f7a1681
44951+
languageName: node
44952+
linkType: hard
44953+
4494544954
"zod@npm:3.23.8":
4494644955
version: 3.23.8
4494744956
resolution: "zod@npm:3.23.8"

0 commit comments

Comments
 (0)