langchain
Version:
Typescript bindings for langchain
292 lines (290 loc) • 11.8 kB
JavaScript
import { isBaseChatModel, isConfigurableModel } from "./model.js";
import { StructuredOutputParsingError } from "./errors.js";
import { isInteropZodObject, isInteropZodSchema } from "@langchain/core/utils/types";
import { Validator, toJsonSchema } from "@langchain/core/utils/json_schema";
//#region src/agents/responses.ts
/**
* Default value for strict mode in providerStrategy.
*
* When using providerStrategy with json_schema response format, OpenAI's parse() method
* requires all function tools to have strict: true. This ensures the model's output
* exactly matches the provided JSON schema.
*
* @see https://platform.openai.com/docs/guides/structured-outputs
*/
const PROVIDER_STRATEGY_DEFAULT_STRICT = true;
/**
* This is a global counter for generating unique names for tools.
*/
let bindingIdentifier = 0;
/**
* Information for tracking structured output tool metadata.
* This contains all necessary information to handle structured responses generated
* via tool calls, including the original schema, its type classification, and the
* corresponding tool implementation used by the tools strategy.
*/
var ToolStrategy = class ToolStrategy {
constructor(schema, tool, options) {
this.schema = schema;
this.tool = tool;
this.options = options;
}
get name() {
return this.tool.function.name;
}
static fromSchema(schema, outputOptions) {
/**
* It is required for tools to have a name so we can map the tool call to the correct tool
* when parsing the response.
*/
function getFunctionName(name) {
return name ?? `extract-${++bindingIdentifier}`;
}
if (isInteropZodSchema(schema)) {
const asJsonSchema$1 = toJsonSchema(schema);
const tool$1 = {
type: "function",
function: {
name: getFunctionName(asJsonSchema$1.title),
strict: false,
description: asJsonSchema$1.description ?? "Tool for extracting structured output from the model's response.",
parameters: asJsonSchema$1
}
};
return new ToolStrategy(asJsonSchema$1, tool$1, outputOptions);
}
let functionDefinition;
if (typeof schema.name === "string" && typeof schema.parameters === "object" && schema.parameters != null) functionDefinition = schema;
else functionDefinition = {
name: getFunctionName(schema.title),
description: schema.description ?? "",
parameters: schema.schema || schema
};
const asJsonSchema = toJsonSchema(schema);
const tool = {
type: "function",
function: functionDefinition
};
return new ToolStrategy(asJsonSchema, tool, outputOptions);
}
/**
* Parse tool arguments according to the schema.
*
* @throws {StructuredOutputParsingError} if the response is not valid
* @param toolArgs - The arguments from the tool call
* @returns The parsed response according to the schema type
*/
parse(toolArgs) {
const validator = new Validator(this.schema);
const result = validator.validate(toolArgs);
if (!result.valid) throw new StructuredOutputParsingError(this.name, result.errors.map((e) => e.error));
return toolArgs;
}
};
var ProviderStrategy = class ProviderStrategy {
_schemaType;
/**
* The schema to use for the provider strategy
*/
schema;
/**
* Whether to use strict mode for the provider strategy
*/
strict;
constructor(schemaOrOptions, strict) {
if ("schema" in schemaOrOptions && typeof schemaOrOptions.schema === "object" && schemaOrOptions.schema !== null && !("type" in schemaOrOptions)) {
const options = schemaOrOptions;
this.schema = options.schema;
this.strict = options.strict ?? PROVIDER_STRATEGY_DEFAULT_STRICT;
} else {
this.schema = schemaOrOptions;
this.strict = strict ?? PROVIDER_STRATEGY_DEFAULT_STRICT;
}
}
static fromSchema(schema, strict) {
const asJsonSchema = toJsonSchema(schema);
return new ProviderStrategy(asJsonSchema, strict);
}
/**
* Parse tool arguments according to the schema. If the response is not valid, return undefined.
*
* @param response - The AI message response to parse
* @returns The parsed response according to the schema type
*/
parse(response) {
/**
* Extract text content from the response.
* Handles both string content and array content (e.g., from thinking models).
*/
let textContent;
if (typeof response.content === "string") textContent = response.content;
else if (Array.isArray(response.content)) {
/**
* For thinking models, content is an array with thinking blocks and text blocks.
* Extract the text from text blocks.
*/
for (const block of response.content) if (typeof block === "object" && block !== null && "type" in block && block.type === "text" && "text" in block && typeof block.text === "string") {
textContent = block.text;
break;
}
}
if (!textContent || textContent === "") return;
try {
const content = JSON.parse(textContent);
const validator = new Validator(this.schema);
const result = validator.validate(content);
if (!result.valid) return;
return content;
} catch {}
}
};
/**
* Handle user input for `responseFormat` parameter of `CreateAgentParams`.
* This function defines the default behavior for the `responseFormat` parameter, which is:
*
* - if value is a Zod schema, default to structured output via tool calling
* - if value is a JSON schema, default to structured output via tool calling
* - if value is a custom response format, return it as is
* - if value is an array, ensure all array elements are instance of `ToolStrategy`
* @param responseFormat - The response format to transform, provided by the user
* @param options - The response format options for tool strategy
* @param model - The model to check if it supports JSON schema output
* @returns
*/
function transformResponseFormat(responseFormat, options, model) {
if (!responseFormat) return [];
if (typeof responseFormat === "object" && responseFormat !== null && "__responseFormatUndefined" in responseFormat) return [];
/**
* If users provide an array, it should only contain raw schemas (Zod or JSON schema),
* not ToolStrategy or ProviderStrategy instances.
*/
if (Array.isArray(responseFormat)) {
/**
* if every entry is a ToolStrategy or ProviderStrategy instance, return the array as is
*/
if (responseFormat.every((item) => item instanceof ToolStrategy || item instanceof ProviderStrategy)) return responseFormat;
/**
* Check if all items are Zod schemas
*/
if (responseFormat.every((item) => isInteropZodObject(item))) return responseFormat.map((item) => ToolStrategy.fromSchema(item, options));
/**
* Check if all items are plain objects (JSON schema)
*/
if (responseFormat.every((item) => typeof item === "object" && item !== null && !isInteropZodObject(item))) return responseFormat.map((item) => ToolStrategy.fromSchema(item, options));
throw new Error("Invalid response format: list contains mixed types.\nAll items must be either InteropZodObject or plain JSON schema objects.");
}
if (responseFormat instanceof ToolStrategy || responseFormat instanceof ProviderStrategy) return [responseFormat];
const useProviderStrategy = hasSupportForJsonSchemaOutput(model);
/**
* `responseFormat` is a Zod schema
*/
if (isInteropZodObject(responseFormat)) return useProviderStrategy ? [ProviderStrategy.fromSchema(responseFormat)] : [ToolStrategy.fromSchema(responseFormat, options)];
/**
* Handle plain object (JSON schema)
*/
if (typeof responseFormat === "object" && responseFormat !== null && "properties" in responseFormat) return useProviderStrategy ? [ProviderStrategy.fromSchema(responseFormat)] : [ToolStrategy.fromSchema(responseFormat, options)];
throw new Error(`Invalid response format: ${String(responseFormat)}`);
}
/**
* Creates a tool strategy for structured output using function calling.
*
* This function configures structured output by converting schemas into function tools that
* the model calls. Unlike `providerStrategy`, which uses native JSON schema support,
* `toolStrategy` works with any model that supports function calling, making it more
* widely compatible across providers and model versions.
*
* The model will call a function with arguments matching your schema, and the agent will
* extract and validate the structured output from the tool call. This approach is automatically
* used when your model doesn't support native JSON schema output.
*
* @param responseFormat - The schema(s) to enforce. Can be a single Zod schema, an array of Zod schemas,
* a JSON schema object, or an array of JSON schema objects.
* @param options - Optional configuration for the tool strategy
* @param options.handleError - How to handle errors when the model calls multiple structured output tools
* or when the output doesn't match the schema. Defaults to `true` (auto-retry). Can be `false` (throw),
* a `string` (retry with message), or a `function` (custom handler).
* @param options.toolMessageContent - Custom message content to include in conversation history
* when structured output is generated via tool call
* @returns A `TypedToolStrategy` instance that can be used as the `responseFormat` in `createAgent`
*
* @example
* ```ts
* import { toolStrategy, createAgent } from "langchain";
* import { z } from "zod";
*
* const agent = createAgent({
* model: "claude-haiku-4-5",
* responseFormat: toolStrategy(
* z.object({
* answer: z.string(),
* confidence: z.number().min(0).max(1),
* })
* ),
* });
* ```
*
* @example
* ```ts
* // Multiple schemas - model can choose which one to use
* const agent = createAgent({
* model: "claude-haiku-4-5",
* responseFormat: toolStrategy([
* z.object({ name: z.string(), age: z.number() }),
* z.object({ email: z.string(), phone: z.string() }),
* ]),
* });
* ```
*/
function toolStrategy(responseFormat, options) {
return transformResponseFormat(responseFormat, options);
}
function providerStrategy(responseFormat) {
/**
* Handle options object format
*/
if (typeof responseFormat === "object" && responseFormat !== null && "schema" in responseFormat && !isInteropZodSchema(responseFormat) && !("type" in responseFormat)) {
const { schema, strict: strictFlag } = responseFormat;
return ProviderStrategy.fromSchema(schema, strictFlag);
}
/**
* Handle direct schema format
*/
return ProviderStrategy.fromSchema(responseFormat);
}
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
*/
function hasSupportForJsonSchemaOutput(model) {
if (!model) return false;
if (typeof model === "string") {
const modelName = model.split(":").pop();
return MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.some((modelNameSnippet) => modelName.includes(modelNameSnippet));
}
if (isConfigurableModel(model)) {
const configurableModel = model;
return hasSupportForJsonSchemaOutput(configurableModel._defaultConfig.model);
}
if (!isBaseChatModel(model)) return false;
const chatModelClass = model.getName();
/**
* for testing purposes only
*/
if (chatModelClass === "FakeToolCallingChatModel") return true;
if (CHAT_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.includes(chatModelClass) && ("model" in model && MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.some((modelNameSnippet) => typeof model.model === "string" && model.model.includes(modelNameSnippet)) || chatModelClass === "FakeToolCallingModel" && "structuredResponse" in model)) return true;
return false;
}
//#endregion
export { ProviderStrategy, ToolStrategy, providerStrategy, toolStrategy, transformResponseFormat };
//# sourceMappingURL=responses.js.map