core[patch]: Fix handling of title when tool schema is specified manually via JSONSchema (#30479)

Fix issue: https://github.com/langchain-ai/langchain/issues/30456
This commit is contained in:
Eugene Yurtsev 2025-03-25 15:15:24 -04:00 committed by GitHub
parent c5e42a4027
commit 0acca6b9c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 100 additions and 3 deletions

View File

@ -62,17 +62,36 @@ class ToolDescription(TypedDict):
def _rm_titles(kv: dict, prev_key: str = "") -> dict:
"""Recursively removes "title" fields from a JSON schema dictionary.
Remove "title" fields from the input JSON schema dictionary,
except when a "title" appears within a property definition under "properties".
Args:
kv (dict): The input JSON schema as a dictionary.
prev_key (str): The key from the parent dictionary, used to identify context.
Returns:
dict: A new dictionary with appropriate "title" fields removed.
"""
new_kv = {}
for k, v in kv.items():
if k == "title":
if isinstance(v, dict) and prev_key == "properties" and "title" in v:
# If the value is a nested dict and part of a property under "properties",
# preserve the title but continue recursion
if isinstance(v, dict) and prev_key == "properties":
new_kv[k] = _rm_titles(v, k)
else:
# Otherwise, remove this "title" key
continue
elif isinstance(v, dict):
# Recurse into nested dictionaries
new_kv[k] = _rm_titles(v, k)
else:
# Leave non-dict values untouched
new_kv[k] = v
return new_kv

View File

@ -60,7 +60,10 @@ from langchain_core.tools.base import (
_is_message_content_type,
get_all_basemodel_annotations,
)
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_core.utils.function_calling import (
convert_to_openai_function,
convert_to_openai_tool,
)
from langchain_core.utils.pydantic import (
PYDANTIC_MAJOR_VERSION,
_create_subset_model,
@ -2560,3 +2563,44 @@ def test_tool_decorator_description() -> None:
]
== "description"
)
def test_title_property_preserved() -> None:
"""Test that the title property is preserved when generating schema.
https://github.com/langchain-ai/langchain/issues/30456
"""
from typing import Any
from langchain_core.tools import tool
schema_to_be_extracted = {
"type": "object",
"required": [],
"properties": {
"title": {"type": "string", "description": "item title"},
"due_date": {"type": "string", "description": "item due date"},
},
"description": "foo",
}
@tool(args_schema=schema_to_be_extracted)
def extract_data(extracted_data: dict[str, Any]) -> dict[str, Any]:
"""Some documentation."""
return extracted_data
assert convert_to_openai_tool(extract_data) == {
"function": {
"description": "Some documentation.",
"name": "extract_data",
"parameters": {
"properties": {
"due_date": {"description": "item due date", "type": "string"},
"title": {"description": "item title", "type": "string"},
},
"required": [],
"type": "object",
},
},
"type": "function",
}

View File

@ -190,10 +190,44 @@ schema4 = {
"required": ["properties"],
}
schema5 = {
"description": "A list of data.",
"items": {
"description": "foo",
"properties": {
"title": {"type": "string", "description": "item title"},
"due_date": {"type": "string", "description": "item due date"},
},
"required": [],
"type": "object",
},
"type": "array",
}
output5 = {
"description": "A list of data.",
"items": {
"description": "foo",
"properties": {
"title": {"type": "string", "description": "item title"},
"due_date": {"type": "string", "description": "item due date"},
},
"required": [],
"type": "object",
},
"type": "array",
}
@pytest.mark.parametrize(
"schema, output",
[(schema1, output1), (schema2, output2), (schema3, output3), (schema4, output4)],
[
(schema1, output1),
(schema2, output2),
(schema3, output3),
(schema4, output4),
(schema5, output5),
],
)
def test_rm_titles(schema: dict, output: dict) -> None:
assert _rm_titles(schema) == output