11import {
22 AIMessage ,
3+ AIMessageChunk ,
34 UsageMetadata ,
45 type BaseMessage ,
56} from "@langchain/core/messages" ;
@@ -19,7 +20,6 @@ import {
1920// @ts -ignore CJS type resolution workaround
2021import { Ollama } from "ollama/browser" ;
2122import { ChatGenerationChunk , ChatResult } from "@langchain/core/outputs" ;
22- import { AIMessageChunk } from "@langchain/core/messages" ;
2323import type {
2424 ChatRequest as OllamaChatRequest ,
2525 ChatResponse as OllamaChatResponse ,
@@ -28,6 +28,7 @@ import type {
2828} from "ollama" ;
2929import {
3030 Runnable ,
31+ RunnableLambda ,
3132 RunnablePassthrough ,
3233 RunnableSequence ,
3334} from "@langchain/core/runnables" ;
@@ -40,6 +41,7 @@ import {
4041import {
4142 InteropZodType ,
4243 isInteropZodSchema ,
44+ interopParseAsync ,
4345} from "@langchain/core/utils/types" ;
4446import { toJsonSchema } from "@langchain/core/utils/json_schema" ;
4547import {
@@ -680,9 +682,9 @@ export class ChatOllama
680682 runManager
681683 ) ) {
682684 if ( ! finalChunk ) {
683- finalChunk = chunk . message ;
685+ finalChunk = chunk . message as AIMessageChunk ;
684686 } else {
685- finalChunk = concat ( finalChunk , chunk . message ) ;
687+ finalChunk = concat ( finalChunk , chunk . message as AIMessageChunk ) ;
686688 }
687689 }
688690
@@ -836,11 +838,12 @@ export class ChatOllama
836838 const jsonSchema = outputSchemaIsZod
837839 ? toJsonSchema ( outputSchema )
838840 : outputSchema ;
841+ const functionName = config ?. name ?? "extract" ;
839842 const llm = this . bindTools ( [
840843 {
841844 type : "function" as const ,
842845 function : {
843- name : "extract" ,
846+ name : functionName ,
844847 description : jsonSchema . description ,
845848 parameters : jsonSchema ,
846849 } ,
@@ -852,9 +855,68 @@ export class ChatOllama
852855 schema : toJsonSchema ( outputSchema ) ,
853856 } ,
854857 } ) ;
855- const outputParser = outputSchemaIsZod
856- ? StructuredOutputParser . fromZodSchema ( outputSchema )
857- : new JsonOutputParser < RunOutput > ( ) ;
858+
859+ /**
860+ * Create a parser that handles both tool calls and JSON content
861+ */
862+ const outputParser = RunnableLambda . from < BaseMessage , RunOutput > (
863+ async ( input : BaseMessage ) : Promise < RunOutput > => {
864+ /**
865+ * Ensure input is an AI message (either AIMessage or AIMessageChunk)
866+ */
867+ if (
868+ ! AIMessage . isInstance ( input ) &&
869+ ! AIMessageChunk . isInstance ( input )
870+ ) {
871+ throw new Error ( "Input is not an AIMessage or AIMessageChunk." ) ;
872+ }
873+
874+ /**
875+ * First, check if there are tool calls - extract args from the tool call
876+ */
877+ if ( input . tool_calls && input . tool_calls . length > 0 ) {
878+ const toolCall = input . tool_calls . find (
879+ ( tc ) => tc . name === functionName
880+ ) ;
881+ if ( toolCall && toolCall . args ) {
882+ /**
883+ * Validate with schema if Zod schema is provided
884+ */
885+ if ( outputSchemaIsZod ) {
886+ return await interopParseAsync (
887+ outputSchema as InteropZodType < RunOutput > ,
888+ toolCall . args
889+ ) ;
890+ }
891+ return toolCall . args as RunOutput ;
892+ }
893+ }
894+
895+ /**
896+ * Fallback: parse content as JSON (when format: "json" is set)
897+ */
898+ const content =
899+ typeof input . content === "string" ? input . content : "" ;
900+ if ( ! content ) {
901+ throw new Error (
902+ "No tool calls found and content is empty. Cannot parse structured output."
903+ ) ;
904+ }
905+
906+ /**
907+ * Use the appropriate parser based on schema type
908+ */
909+ if ( outputSchemaIsZod ) {
910+ const zodParser = StructuredOutputParser . fromZodSchema (
911+ outputSchema as InteropZodType < RunOutput >
912+ ) ;
913+ return await zodParser . parse ( content ) ;
914+ } else {
915+ const jsonParser = new JsonOutputParser < RunOutput > ( ) ;
916+ return await jsonParser . parse ( content ) ;
917+ }
918+ }
919+ ) ;
858920
859921 if ( ! config ?. includeRaw ) {
860922 return llm . pipe ( outputParser ) as Runnable <
0 commit comments