Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 10 additions & 8 deletions dspy/utils/langchain_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

def convert_langchain_tool(tool: "BaseTool") -> Tool:
"""Build a DSPy tool from a LangChain tool.

This function converts a LangChain tool (either created with @tool decorator
or by subclassing BaseTool) into a DSPy Tool.

Expand All @@ -29,13 +29,15 @@ async def func(**kwargs):
# Get args_schema from the tool
# https://python.langchain.com/api_reference/core/tools/langchain_core.tools.base.BaseTool.html#langchain_core.tools.base.BaseTool.args_schema
args_schema = tool.args_schema
args, _, arg_desc = convert_input_schema_to_tool_args(args_schema.model_json_schema())

# The args_schema of Langchain tool is a pydantic model, so we can get the type hints from the model fields
arg_types = {
key: field.annotation if field.annotation is not None else Any
for key, field in args_schema.model_fields.items()
}
if isinstance(args_schema, dict):
args, arg_types, arg_desc = convert_input_schema_to_tool_args(args_schema)
Comment thread
greptile-apps[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Required arguments change

The dictionary path does not preserve the schema's required list when advertising the tool. With required: ["query"] and an optional limit without a default, format_as_litellm_function_call() advertises both arguments as required. A required property with a default is advertised as optional instead. Both cases reproduce with a real StructuredTool: query-only input works despite the advertised requirement for limit, while an empty input accepted by the advertised schema fails execution because query is missing. Preserve the explicit required list and test both cases before merging; the new test only covers defaults that happen to match requiredness.

Artifacts

Evidence from the check

  • Preserved the full authored and executed StructuredTool harness locally; remote upload is blocked by the absence of an upload capability.

Command output from the check

  • Preserved the captured parent execution showing successful LangChain calls and dictionary-schema AttributeError; remote upload remains blocked.

Command output from the check

  • Preserved the captured head execution showing the nullable-schema TypeError and successful scalar control; remote upload remains blocked.

Evidence from the check

  • The previously executed Python harness remains at this local path; remote upload cannot be confirmed because no upload capability is available.

Evidence from the check

  • The previously executed shell capture script remains at this local path; remote upload cannot be confirmed because no upload capability is available.

Command output from the check

  • The captured parent execution records both dictionary conversion failures and source-contract checks; the observed evidence is preserved locally.

Command output from the check

  • The captured head execution records both required-field mismatches and validation and invocation results; the observed evidence is preserved locally.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed both requiredness mismatches independently. dc14339 preserves a detached copy of the original dict schema for native advertisement and whole-argument validation. required-with-default stays required; optional-without-default stays optional; missing required and forbidden additional properties reject before ainvoke. Existing function/Pydantic schema inference stays unchanged. Regression asserts exact advertised schema and successful invocation only for valid arguments. Full corrective diff reviewed before push; remote acceptance still pending.

else:
args, _, arg_desc = convert_input_schema_to_tool_args(args_schema.model_json_schema())
# Preserve model annotations for nested Pydantic arguments.
arg_types = {
key: field.annotation if field.annotation is not None else Any
for key, field in args_schema.model_fields.items()
}

return Tool(
func=func,
Expand Down
36 changes: 36 additions & 0 deletions tests/utils/test_langchain_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,39 @@ def get_age(profile: Profile) -> int:
assert tool.arg_types == {"profile": Profile}
assert tool.arg_desc == {"profile": "No description provided. (Required)"}
assert await tool.acall(profile=Profile(name="John", age=20)) == 20


@pytest.mark.asyncio
@pytest.mark.extra
async def test_convert_tool_with_json_schema_dict():
from langchain_core.tools import StructuredTool

schema = {
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 5},
},
"required": ["query"],
}

calls = []

async def search(query: str, limit: int = 5) -> str:
calls.append((query, limit))
return f"{query}:{limit}"

langchain_tool = StructuredTool.from_function(
coroutine=search,
name="search",
description="Search records.",
args_schema=schema,
)
dspy_tool = convert_langchain_tool(langchain_tool)

assert await dspy_tool.acall(query="cats") == "cats:5"
assert dspy_tool.args == schema["properties"]
assert dspy_tool.format_as_litellm_function_call()["function"]["parameters"]["required"] == ["query"]
with pytest.raises(ValueError, match="Arg query"):
await dspy_tool.acall(query=3)
assert calls == [("cats", 5)]