fix(langgraph): respect strict option in responseFormat inside createReactAgent (#1389)

Co-authored-by: Tat Dat Duong <david@duong.cz>
This commit is contained in:
sdip0971
2025-07-16 18:06:29 +05:30
committed by GitHub
parent 4aed3f4362
commit 6e616f5da1
3 changed files with 37 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@langchain/langgraph": patch
---
fix(langgraph): respect strict option in responseFormat inside createReactAgent
@@ -57,10 +57,13 @@ export interface AgentState<
export type N = typeof START | "agent" | "tools";
export type StructuredResponseSchemaAndPrompt<StructuredResponseType> = {
prompt: string;
type StructuredResponseSchemaOptions<StructuredResponseType> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema: InteropZodType<StructuredResponseType> | Record<string, any>;
prompt?: string;
strict?: boolean;
[key: string]: unknown;
};
function _convertMessageModifierToPrompt(
@@ -478,6 +481,7 @@ export type CreateReactAgentParams<
* @deprecated Use prompt instead.
*/
stateModifier?: StateModifier;
/**
* An optional prompt for the LLM. This takes full graph state BEFORE the LLM is called and prepares the input to LLM.
*
@@ -523,13 +527,15 @@ export type CreateReactAgentParams<
*/
responseFormat?:
| InteropZodType<StructuredResponseType>
| StructuredResponseSchemaAndPrompt<StructuredResponseType>
| StructuredResponseSchemaOptions<StructuredResponseType>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| Record<string, any>;
/**
* An optional name for the agent.
*/
name?: string;
/**
* Use to specify how to expose the agent name to the underlying supervisor LLM.
@@ -711,20 +717,18 @@ export function createReactAgent<
const messages = [...state.messages];
let modelWithStructuredOutput;
if (
typeof responseFormat === "object" &&
"prompt" in responseFormat &&
"schema" in responseFormat
) {
const { prompt, schema } = responseFormat;
modelWithStructuredOutput = (await _getModel(llm)).withStructuredOutput(
schema
);
messages.unshift(new SystemMessage({ content: prompt }));
const model = await _getModel(llm);
if (typeof responseFormat === "object" && "schema" in responseFormat) {
const { prompt, schema, ...options } =
responseFormat as StructuredResponseSchemaOptions<StructuredResponseFormat>;
modelWithStructuredOutput = model.withStructuredOutput(schema, options);
if (prompt != null) {
messages.unshift(new SystemMessage({ content: prompt }));
}
} else {
modelWithStructuredOutput = (await _getModel(llm)).withStructuredOutput(
responseFormat
);
modelWithStructuredOutput = model.withStructuredOutput(responseFormat);
}
const response = await modelWithStructuredOutput.invoke(messages, config);
+12 -1
View File
@@ -1864,20 +1864,31 @@ describe("createReactAgent with structured responses", () => {
"The weather is nice"
);
// Test with prompt and schema
// Test with prompt and schema (and options)
const agent2 = createReactAgent({
llm,
tools: [getWeather],
responseFormat: {
prompt: "Meow",
schema: weatherResponseSchema,
name: "generate_structured_response",
strict: true,
},
});
const withStructuredOutputSpy = vi.spyOn(llm, "withStructuredOutput");
const result2 = await agent2.invoke({
messages: [new HumanMessage("What's the weather?")],
});
// Check if `strict` is set properly
expect(withStructuredOutputSpy).toHaveBeenCalledWith(
weatherResponseSchema,
{ name: "generate_structured_response", strict: true }
);
expect(result2.structuredResponse).toEqual(expectedStructuredResponse);
expect(result2.messages.length).toEqual(4);
expect(result2.messages[2].content).toEqual(