Skip to content

Commit bd1b5d1

Browse files
committed
feat(ollama): Add support for native structured outputs
Signed-off-by: Jonghwan Hyeon <[email protected]>
1 parent f499d2a commit bd1b5d1

File tree

1 file changed

+115
-109
lines changed

1 file changed

+115
-109
lines changed

libs/providers/langchain-ollama/src/chat_models.ts

Lines changed: 115 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import {
88
BaseLanguageModelInput,
99
StructuredOutputMethodOptions,
10+
FunctionDefinition,
1011
} from "@langchain/core/language_models/base";
1112
import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
1213
import {
@@ -28,7 +29,6 @@ import type {
2829
} from "ollama";
2930
import {
3031
Runnable,
31-
RunnableLambda,
3232
RunnablePassthrough,
3333
RunnableSequence,
3434
} from "@langchain/core/runnables";
@@ -41,14 +41,14 @@ import {
4141
import {
4242
InteropZodType,
4343
isInteropZodSchema,
44-
interopParseAsync,
4544
} from "@langchain/core/utils/types";
4645
import { toJsonSchema } from "@langchain/core/utils/json_schema";
4746
import {
4847
convertOllamaMessagesToLangChain,
4948
convertToOllamaMessages,
5049
} from "./utils.js";
5150
import { OllamaCamelCaseOptions } from "./types.js";
51+
import { JsonOutputKeyToolsParser } from "@langchain/core/output_parsers/openai_tools";
5252

5353
export interface ChatOllamaCallOptions extends BaseChatModelCallOptions {
5454
/**
@@ -834,121 +834,127 @@ export class ChatOllama
834834
parsed: RunOutput;
835835
}
836836
> {
837-
if (config?.method === undefined || config?.method === "jsonSchema") {
838-
const outputSchemaIsZod = isInteropZodSchema(outputSchema);
839-
const jsonSchema = outputSchemaIsZod
840-
? toJsonSchema(outputSchema)
841-
: outputSchema;
842-
const functionName = config?.name ?? "extract";
843-
const llm = this.bindTools([
844-
{
845-
type: "function" as const,
846-
function: {
837+
let llm: Runnable<BaseLanguageModelInput>;
838+
let outputParser: Runnable<AIMessageChunk, RunOutput>;
839+
840+
const { schema, name, includeRaw } = {
841+
...config,
842+
schema: outputSchema,
843+
};
844+
const method = config?.method ?? "jsonSchema";
845+
846+
if (method === "functionCalling") {
847+
let functionName = name ?? "extract";
848+
if (isInteropZodSchema(schema)) {
849+
const jsonSchema = toJsonSchema(schema);
850+
llm = this.bindTools([
851+
{
852+
type: "function",
853+
function: {
854+
name: functionName,
855+
description: jsonSchema.description,
856+
parameters: jsonSchema,
857+
},
858+
},
859+
]).withConfig({
860+
ls_structured_output_format: {
861+
kwargs: { method },
862+
schema: jsonSchema,
863+
},
864+
} as Partial<ChatOllamaCallOptions>);
865+
outputParser = new JsonOutputKeyToolsParser({
866+
returnSingle: true,
867+
keyName: functionName,
868+
zodSchema: schema,
869+
});
870+
} else {
871+
let openAIFunctionDefinition: FunctionDefinition;
872+
if (
873+
typeof schema.name === "string" &&
874+
typeof schema.parameters === "object" &&
875+
schema.parameters != null
876+
) {
877+
openAIFunctionDefinition = schema as FunctionDefinition;
878+
functionName = schema.name;
879+
} else {
880+
openAIFunctionDefinition = {
847881
name: functionName,
848-
description: jsonSchema.description,
849-
parameters: jsonSchema,
882+
description: schema.description ?? "",
883+
parameters: schema,
884+
};
885+
}
886+
llm = this.bindTools([
887+
{
888+
type: "function",
889+
function: openAIFunctionDefinition,
850890
},
851-
},
852-
]).withConfig({
891+
]).withConfig({
892+
ls_structured_output_format: {
893+
kwargs: { method },
894+
schema,
895+
},
896+
} as Partial<ChatOllamaCallOptions>);
897+
outputParser = new JsonOutputKeyToolsParser<RunOutput>({
898+
returnSingle: true,
899+
keyName: functionName,
900+
});
901+
}
902+
} else if (method === "jsonMode") {
903+
outputParser = isInteropZodSchema(schema)
904+
? StructuredOutputParser.fromZodSchema(schema)
905+
: new JsonOutputParser<RunOutput>();
906+
const jsonSchema = toJsonSchema(schema);
907+
llm = this.withConfig({
853908
format: "json",
854909
ls_structured_output_format: {
855-
kwargs: { method: "jsonSchema" },
856-
schema: toJsonSchema(outputSchema),
910+
kwargs: { method },
911+
schema: jsonSchema,
857912
},
858-
});
859-
860-
/**
861-
* Create a parser that handles both tool calls and JSON content
862-
*/
863-
const outputParser = RunnableLambda.from<BaseMessage, RunOutput>(
864-
async (input: BaseMessage): Promise<RunOutput> => {
865-
/**
866-
* Ensure input is an AI message (either AIMessage or AIMessageChunk)
867-
*/
868-
if (
869-
!AIMessage.isInstance(input) &&
870-
!AIMessageChunk.isInstance(input)
871-
) {
872-
throw new Error("Input is not an AIMessage or AIMessageChunk.");
873-
}
874-
875-
/**
876-
* First, check if there are tool calls - extract args from the tool call
877-
*/
878-
if (input.tool_calls && input.tool_calls.length > 0) {
879-
const toolCall = input.tool_calls.find(
880-
(tc) => tc.name === functionName
881-
);
882-
if (toolCall && toolCall.args) {
883-
/**
884-
* Validate with schema if Zod schema is provided
885-
*/
886-
if (outputSchemaIsZod) {
887-
return await interopParseAsync(
888-
outputSchema as InteropZodType<RunOutput>,
889-
toolCall.args
890-
);
891-
}
892-
return toolCall.args as RunOutput;
893-
}
894-
}
895-
896-
/**
897-
* Fallback: parse content as JSON (when format: "json" is set)
898-
*/
899-
const content =
900-
typeof input.content === "string" ? input.content : "";
901-
if (!content) {
902-
throw new Error(
903-
"No tool calls found and content is empty. Cannot parse structured output."
904-
);
905-
}
906-
907-
/**
908-
* Use the appropriate parser based on schema type
909-
*/
910-
if (outputSchemaIsZod) {
911-
const zodParser = StructuredOutputParser.fromZodSchema(
912-
outputSchema as InteropZodType<RunOutput>
913-
);
914-
return await zodParser.parse(content);
915-
} else {
916-
const jsonParser = new JsonOutputParser<RunOutput>();
917-
return await jsonParser.parse(content);
918-
}
919-
}
913+
} as Partial<ChatOllamaCallOptions>);
914+
} else if (method === "jsonSchema") {
915+
outputParser = isInteropZodSchema(schema)
916+
? StructuredOutputParser.fromZodSchema(schema)
917+
: new JsonOutputParser<RunOutput>();
918+
const jsonSchema = toJsonSchema(schema);
919+
llm = this.withConfig({
920+
format: jsonSchema,
921+
ls_structured_output_format: {
922+
kwargs: { method },
923+
schema: jsonSchema,
924+
},
925+
} as Partial<ChatOllamaCallOptions>);
926+
} else {
927+
throw new TypeError(
928+
`Unrecognized structured output method '${method}'. Expected one of 'functionCalling', 'jsonSchema', or 'jsonMode'`
920929
);
930+
}
921931

922-
if (!config?.includeRaw) {
923-
return llm.pipe(outputParser) as Runnable<
924-
BaseLanguageModelInput,
925-
RunOutput
926-
>;
927-
}
932+
if (!includeRaw) {
933+
return llm.pipe(outputParser).withConfig({
934+
runName: "ChatOllamaStructuredOutput",
935+
}) as Runnable<BaseLanguageModelInput, RunOutput>;
936+
}
928937

929-
const parserAssign = RunnablePassthrough.assign({
930-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
931-
parsed: (input: any, config) => outputParser.invoke(input.raw, config),
932-
});
933-
const parserNone = RunnablePassthrough.assign({
934-
parsed: () => null,
935-
});
936-
const parsedWithFallback = parserAssign.withFallbacks({
937-
fallbacks: [parserNone],
938-
});
939-
return RunnableSequence.from<
940-
BaseLanguageModelInput,
941-
{ raw: BaseMessage; parsed: RunOutput }
942-
>([
943-
{
944-
raw: llm,
945-
},
946-
parsedWithFallback,
947-
]);
948-
} else {
949-
// TODO: Fix this type in core
938+
const parserAssign = RunnablePassthrough.assign({
950939
// eslint-disable-next-line @typescript-eslint/no-explicit-any
951-
return super.withStructuredOutput<RunOutput>(outputSchema, config as any);
952-
}
940+
parsed: (input: any, config) => outputParser.invoke(input.raw, config),
941+
});
942+
const parserNone = RunnablePassthrough.assign({
943+
parsed: () => null,
944+
});
945+
const parsedWithFallback = parserAssign.withFallbacks({
946+
fallbacks: [parserNone],
947+
});
948+
return RunnableSequence.from<
949+
BaseLanguageModelInput,
950+
{ raw: BaseMessage; parsed: RunOutput }
951+
>([
952+
{
953+
raw: llm,
954+
},
955+
parsedWithFallback,
956+
]).withConfig({
957+
runName: "StructuredOutputRunnable",
958+
});
953959
}
954960
}

0 commit comments

Comments
 (0)