Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions libs/langchain/src/agents/model.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type { LanguageModelLike } from "@langchain/core/language_models/base";
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";

export interface ConfigurableModelInterface {
_queuedMethodOperations: Record<string, unknown>;
_model: () => Promise<BaseChatModel>;
}
import type { ConfigurableModel } from "../chat_models/universal.js";

export function isBaseChatModel(
model: LanguageModelLike
Expand All @@ -18,7 +15,7 @@ export function isBaseChatModel(

export function isConfigurableModel(
model: unknown
): model is ConfigurableModelInterface {
): model is ConfigurableModel {
return (
typeof model === "object" &&
model != null &&
Expand Down
83 changes: 38 additions & 45 deletions libs/langchain/src/agents/nodes/AgentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Runnable, RunnableConfig } from "@langchain/core/runnables";
import { BaseMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
import { Command, type LangGraphRunnableConfig } from "@langchain/langgraph";
import { type LanguageModelLike } from "@langchain/core/language_models/base";
import { type BaseChatModelCallOptions } from "@langchain/core/language_models/chat_models";
import {
type BaseChatModel,
type BaseChatModelCallOptions,
} from "@langchain/core/language_models/chat_models";
import {
InteropZodObject,
getSchemaDescription,
interopParse,
interopZodObjectPartial,
} from "@langchain/core/utils/types";
Expand Down Expand Up @@ -133,16 +135,15 @@ export class AgentNode<
* @param model - The model to get the response format for.
* @returns The response format.
*/
#getResponseFormat(
async #getResponseFormat(
model: string | LanguageModelLike
): ResponseFormat | undefined {
): Promise<ResponseFormat | undefined> {
if (!this.#options.responseFormat) {
return undefined;
}

const strategies = transformResponseFormat(
const strategies = await transformResponseFormat(
this.#options.responseFormat,
undefined,
model
);

Expand Down Expand Up @@ -278,9 +279,11 @@ export class AgentNode<
*/
validateLLMHasNoBoundTools(request.model);

const structuredResponseFormat = this.#getResponseFormat(request.model);
const structuredResponseFormat = await this.#getResponseFormat(
request.model
);
const modelWithTools = await this.#bindTools(
request.model,
request.model as BaseChatModel,
request,
structuredResponseFormat
);
Expand All @@ -293,20 +296,21 @@ export class AgentNode<
const response = (await modelWithTools.invoke(
modelInput,
invokeConfig
)) as AIMessage;
)) as AIMessage | { raw: BaseMessage; parsed: StructuredResponseFormat };

/**
* if the user requests a native schema output, try to parse the response
* and return the structured response if it is valid
* if the user requests a native schema output, we should receive a raw message
* with the structured response
*/
if (structuredResponseFormat?.type === "native") {
const structuredResponse =
structuredResponseFormat.strategy.parse(response);
if (structuredResponse) {
return { structuredResponse, messages: [response] };
if (structuredResponseFormat?.type === "native" || "raw" in response) {
if (!("raw" in response)) {
throw new Error("Response is not a structured response.");
}

return response;
return {
structuredResponse: response.parsed,
messages: [response.raw],
};
}

if (!structuredResponseFormat || !response.tool_calls) {
Expand Down Expand Up @@ -731,7 +735,7 @@ export class AgentNode<
}

async #bindTools(
model: LanguageModelLike,
model: BaseChatModel,
preparedOptions: ModelRequest | undefined,
structuredResponseFormat: ResponseFormat | undefined
): Promise<Runnable> {
Expand Down Expand Up @@ -761,37 +765,26 @@ export class AgentNode<
/**
* check if the user requests a native schema output
*/
if (structuredResponseFormat?.type === "native") {
const jsonSchemaParams = {
name: structuredResponseFormat.strategy.schema?.name ?? "extract",
description: getSchemaDescription(
structuredResponseFormat.strategy.schema
),
schema: structuredResponseFormat.strategy.schema,
strict: true,
};

Object.assign(options, {
response_format: {
type: "json_schema",
json_schema: jsonSchemaParams,
},
ls_structured_output_format: {
kwargs: { method: "json_schema" },
schema: structuredResponseFormat.strategy.schema,
},
strict: true,
});
}
const modelWithStructuredOutput =
structuredResponseFormat?.type === "native"
? model.withStructuredOutput(structuredResponseFormat.strategy.schema, {
includeRaw: true,
method: "jsonSchema",
})
: model;

/**
* Bind tools to the model if they are not already bound.
*/
const modelWithTools = await bindTools(model, allTools, {
...options,
...(preparedOptions?.modelSettings ?? {}),
tool_choice: toolChoice,
});
const modelWithTools = await bindTools(
modelWithStructuredOutput as LanguageModelLike,
allTools,
{
...options,
...(preparedOptions?.modelSettings ?? {}),
tool_choice: toolChoice,
}
);

/**
* Create a model runnable with the prompt and agent name
Expand Down
132 changes: 38 additions & 94 deletions libs/langchain/src/agents/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { type AIMessage } from "@langchain/core/messages";
import { type LanguageModelLike } from "@langchain/core/language_models/base";
import { toJsonSchema, Validator } from "@langchain/core/utils/json_schema";
import { type FunctionDefinition } from "@langchain/core/language_models/base";
import { type BaseChatModel } from "@langchain/core/language_models/chat_models";

import { initChatModel } from "../chat_models/universal.js";
import {
StructuredOutputParsingError,
MultipleStructuredOutputsError,
} from "./errors.js";
import { isConfigurableModel, isBaseChatModel } from "./model.js";
import { isConfigurableModel } from "./model.js";

/**
* Special type to indicate that no response format is provided.
Expand Down Expand Up @@ -206,7 +208,7 @@ export type ResponseFormat = ToolStrategy<any> | ProviderStrategy<any>;
* @param model - The model to check if it supports JSON schema output
* @returns
*/
export function transformResponseFormat(
export async function transformResponseFormat(
responseFormat?:
| InteropZodType<any>
| InteropZodType<any>[]
Expand All @@ -215,9 +217,8 @@ export function transformResponseFormat(
| ResponseFormat
| ToolStrategy<any>[]
| ResponseFormatUndefined,
options?: ToolStrategyOptions,
model?: LanguageModelLike | string
): ResponseFormat[] {
): Promise<ResponseFormat[]> {
if (!responseFormat) {
return [];
}
Expand All @@ -237,62 +238,49 @@ export function transformResponseFormat(
*/
if (Array.isArray(responseFormat)) {
/**
* if every entry is a ToolStrategy or ProviderStrategy instance, return the array as is
* we don't allow to have a list of ProviderStrategy instances
*/
if (
responseFormat.every(
(item) =>
item instanceof ToolStrategy || item instanceof ProviderStrategy
)
) {
return responseFormat as unknown as ResponseFormat[];
}

/**
* Check if all items are Zod schemas
*/
if (responseFormat.every((item) => isInteropZodObject(item))) {
return responseFormat.map((item) =>
ToolStrategy.fromSchema(item as InteropZodObject, options)
if (responseFormat.some((item) => item instanceof ProviderStrategy)) {
throw new Error(
"Invalid response format: list contains ProviderStrategy instances. You can only use a single ProviderStrategy instance instead."
);
}

/**
* Check if all items are plain objects (JSON schema)
* if every entry is a ToolStrategy or ProviderStrategy instance, return the array as is
*/
if (
responseFormat.every(
(item) =>
typeof item === "object" && item !== null && !isInteropZodObject(item)
)
) {
return responseFormat.map((item) =>
ToolStrategy.fromSchema(item as JsonSchemaFormat, options)
);
if (responseFormat.every((item) => item instanceof ToolStrategy)) {
return responseFormat as ResponseFormat[];
}

throw new Error(
`Invalid response format: list contains mixed types.\n` +
`Invalid response format: list contains invalid values.\n` +
`All items must be either InteropZodObject or plain JSON schema objects.`
);
}

/**
* if the response format is a ToolStrategy or ProviderStrategy instance, return it as is
*/
if (
responseFormat instanceof ToolStrategy ||
responseFormat instanceof ProviderStrategy
) {
return [responseFormat];
}

const useProviderStrategy = hasSupportForJsonSchemaOutput(model);
/**
* If nothing is specified we have to check whether the model supports JSON schema output
*/
const useProviderStrategy = await hasSupportForJsonSchemaOutput(model);

/**
* `responseFormat` is a Zod schema
*/
if (isInteropZodObject(responseFormat)) {
return useProviderStrategy
? [ProviderStrategy.fromSchema(responseFormat)]
: [ToolStrategy.fromSchema(responseFormat, options)];
: [ToolStrategy.fromSchema(responseFormat)];
}

/**
Expand All @@ -305,7 +293,7 @@ export function transformResponseFormat(
) {
return useProviderStrategy
? [ProviderStrategy.fromSchema(responseFormat as JsonSchemaFormat)]
: [ToolStrategy.fromSchema(responseFormat as JsonSchemaFormat, options)];
: [ToolStrategy.fromSchema(responseFormat as JsonSchemaFormat)];
}

throw new Error(`Invalid response format: ${String(responseFormat)}`);
Expand Down Expand Up @@ -380,7 +368,12 @@ export function toolStrategy(
| JsonSchemaFormat[],
options?: ToolStrategyOptions
): TypedToolStrategy {
return transformResponseFormat(responseFormat, options) as TypedToolStrategy;
const responseFormatArray = Array.isArray(responseFormat)
? responseFormat
: [responseFormat];
return responseFormatArray.map((item) =>
ToolStrategy.fromSchema(item as InteropZodObject, options)
) as TypedToolStrategy;
}

export function providerStrategy<T extends InteropZodType<any>>(
Expand Down Expand Up @@ -419,76 +412,27 @@ export type JsonSchemaFormat = {
__brand?: never;
};

const CHAT_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT = ["ChatOpenAI", "ChatXAI"];
const MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT = [
"grok",
"gpt-5",
"gpt-4.1",
"gpt-4o",
"gpt-oss",
"o3-pro",
"o3-mini",
];

/**
* Identifies the models that support JSON schema output
* @param model - The model to check
* @returns True if the model supports JSON schema output, false otherwise
*/
export function hasSupportForJsonSchemaOutput(
export async function hasSupportForJsonSchemaOutput(
model?: LanguageModelLike | string
): boolean {
): Promise<boolean> {
if (!model) {
return false;
}

if (typeof model === "string") {
const modelName = model.split(":").pop() as string;
return MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.some(
(modelNameSnippet) => modelName.includes(modelNameSnippet)
);
}

if (isConfigurableModel(model)) {
const configurableModel = model as unknown as {
_defaultConfig: { model: string };
};
return hasSupportForJsonSchemaOutput(
configurableModel._defaultConfig.model
);
}

if (!isBaseChatModel(model)) {
return false;
}
const resolvedModel =
typeof model === "string"
? await initChatModel(model)
: (model as BaseChatModel);

const chatModelClass = model.getName();

/**
* for testing purposes only
*/
if (chatModelClass === "FakeToolCallingChatModel") {
return true;
}

if (
CHAT_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.includes(chatModelClass) &&
/**
* OpenAI models
*/ (("model" in model &&
MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.some(
(modelNameSnippet) =>
typeof model.model === "string" &&
model.model.includes(modelNameSnippet)
)) ||
/**
* for testing purposes only
*/
(chatModelClass === "FakeToolCallingModel" &&
"structuredResponse" in model))
) {
return true;
if (isConfigurableModel(resolvedModel)) {
const profile = await resolvedModel._getProfile();
return profile.structuredOutput ?? false;
}

return false;
return resolvedModel.profile.structuredOutput ?? false;
}
Loading
Loading