Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
b3f8bd2
Output handling refactoring borrowed from output_mode PR
DouweM May 20, 2025
2ebe6a9
Support functions as output_type, as well as lists of functions and o…
DouweM May 20, 2025
ab576d7
Fix tests
DouweM May 20, 2025
6c4fcec
Make Python 3.9 happy
DouweM May 20, 2025
1bd16dc
Support output_type = bound instance method
DouweM May 21, 2025
98e64d4
Support RunContext arg on output_type function using same logic as tools
DouweM May 21, 2025
60d789e
Improve test coverage
DouweM May 21, 2025
14e69d0
Start output tool name disambiguation counter at 2
DouweM May 22, 2025
ee95a80
Stop requiring explicitly specifying type_ kwarg name on ToolOutput
DouweM May 22, 2025
f1f093e
Remove runtime assertion from typed_agent.py as the file is only type…
DouweM May 22, 2025
66e5405
Add typing tests for Agent(output_type=)
DouweM May 22, 2025
53d25f9
Merge remote-tracking branch 'origin/main' into output-type-callable
DouweM May 22, 2025
3aad6fc
Update typing test for mypy
DouweM May 23, 2025
3ff6e74
Treat str in an output_type list the same as in a union
DouweM May 23, 2025
a7fd8ac
Set ToolRetryError as cause on UnexpectedModelBehavior when available
DouweM May 23, 2025
5399df2
Drop end line from example test parameterized test ID to make it easi…
DouweM May 23, 2025
e503edb
Document output functions
DouweM May 23, 2025
f436797
Fix docs
DouweM May 23, 2025
046b813
Update output_type typing tests
DouweM May 23, 2025
dc39e68
Update output_type typing tests
DouweM May 23, 2025
a578745
Document when automatic output_type type inference may fail
DouweM May 23, 2025
fc42d69
Suggested code for from_langchain
matthewfranglen May 26, 2025
7f897b1
Add optional group of langchain to allow type checking
matthewfranglen May 26, 2025
e3b3c7f
Fix linting problems
matthewfranglen May 26, 2025
8597ae4
Add a test for tool conversion
matthewfranglen May 26, 2025
2a92ba0
Drop attempted import of langchain tool
matthewfranglen May 26, 2025
2d22c32
Older pythons don't enjoy type unions with |
matthewfranglen May 26, 2025
2d6cc1a
Drop unnecessary explicit generic parameter on constructor in typing …
DouweM May 26, 2025
43a6247
Merge branch 'main' into langchain-tools
matthewfranglen May 27, 2025
6335103
Merge branch 'output-type-callable' into langchain-tools
matthewfranglen May 27, 2025
6ef816a
hack in the new FunctionSchema approach
matthewfranglen May 27, 2025
aefad98
split into from_function and from_langchain
matthewfranglen May 27, 2025
1614418
Use the proper schema validator
matthewfranglen May 27, 2025
5269059
test the function conversion directly
matthewfranglen May 27, 2025
8b020ec
Merge branch 'main' into langchain-tools
matthewfranglen May 27, 2025
b7478a3
Support async functions for from_function
matthewfranglen May 27, 2025
880c629
Fix the type of the schema
matthewfranglen May 27, 2025
8407f3b
test the langchain function directly
matthewfranglen May 27, 2025
ba90237
type: ignore the funny calls
matthewfranglen May 27, 2025
6351ffa
call the broken function to get coverage
matthewfranglen May 27, 2025
40b5a5c
branch coverage for required/default
matthewfranglen May 27, 2025
25df877
test that the default is overridden
matthewfranglen May 27, 2025
8121577
Change name of function
matthewfranglen May 28, 2025
49d5377
Merge branch 'main' into langchain-tools
matthewfranglen May 28, 2025
ffb78eb
fix import, address PR feedback
matthewfranglen May 28, 2025
8024a80
Self was introduced in python 3.11
matthewfranglen May 28, 2025
98ffd4e
Use AgentDepsT
matthewfranglen May 28, 2025
44f285e
Import Self from typing_extensions
matthewfranglen May 28, 2025
f75ea7b
Merge branch 'main' into langchain-tools
matthewfranglen May 28, 2025
4af3e4e
remove langchain optional group
matthewfranglen May 29, 2025
8bf590b
Make the FunctionSchema arguments defaults
matthewfranglen May 29, 2025
cd5e6c4
sort the items before showing them
matthewfranglen May 29, 2025
e1a5562
use dictionary merging
matthewfranglen May 29, 2025
9194930
Use JsonSchemaValue to describe arguments
matthewfranglen May 29, 2025
9f89e02
Make name and description arguments
matthewfranglen May 29, 2025
d2462d8
Revert unrelated formatting changes
matthewfranglen May 29, 2025
1c99adf
uv run ruff format
matthewfranglen May 29, 2025
c709402
Handle $refs in the langchain schema
matthewfranglen May 29, 2025
f075d7d
Replace arg iteration with assertion
matthewfranglen May 29, 2025
65a04be
Add flag to allow testing additional properties branch
matthewfranglen May 29, 2025
b91cd66
Write langchain tool section HEAVILY BASED ON SMOLAGENTS DOCS
matthewfranglen May 29, 2025
5300f3f
formatting for docs
matthewfranglen May 29, 2025
a06f814
Testing imports in docs is neat
matthewfranglen May 29, 2025
24507f9
import sorting
matthewfranglen May 29, 2025
3ab281f
Rephrase and skip tests over code
matthewfranglen May 30, 2025
e67b0ab
Clarify the requirements over the function
matthewfranglen May 30, 2025
d96fb5b
Add section on from_schema
matthewfranglen May 30, 2025
47e9d7b
replace double quotes with single quotes, add comma
matthewfranglen May 30, 2025
17969fb
docs/tools.md:415:1: I001 [*] Import block is un-sorted or un-formatted
matthewfranglen May 30, 2025
77f6a98
Move langchain tool converter
matthewfranglen Jun 3, 2025
0142858
Add example of using tool from Tool.from_schema
matthewfranglen Jun 3, 2025
77a7bee
remove assertions over _function_tools
matthewfranglen Jun 3, 2025
669b299
Satisfy linter
DouweM Jun 6, 2025
d83ef10
Rename helper to tool_from_langchain
DouweM Jun 6, 2025
fe14acd
Update LangChain example
DouweM Jun 6, 2025
e558fc7
Update LangChain example
DouweM Jun 6, 2025
3646fb3
Merge branch 'main' into langchain-tools
Kludex Jun 6, 2025
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
65 changes: 65 additions & 0 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,42 @@ print(test_model.last_model_request_parameters.function_tools)

_(This example is complete, it can be run "as is")_

If you have a function that lacks appropriate documentation (i.e. poorly named, no type information, poor docstring, use of *args or **kwargs and suchlike) then you can still turn it into a tool that can be effectively used by the agent with the `Tool.from_schema` function. With this you provide the name, description and JSON schema for the function directly:

```python
from pydantic_ai import Agent, Tool
from pydantic_ai.models.test import TestModel


def foobar(**kwargs) -> str:
return kwargs['a'] + kwargs['b']

tool = Tool.from_schema(
function=foobar,
name='sum',
description='Sum two numbers.',
json_schema={
'additionalProperties': False,
'properties': {
'a': {'description': 'the first number', 'type': 'integer'},
'b': {'description': 'the second number', 'type': 'integer'},
},
'required': ['a', 'b'],
'type': 'object',
}
)

test_model = TestModel()
agent = Agent(test_model, tools=[tool])

result = agent.run_sync('testing...')
print(result.output)
#> {"sum":0}
```


Please note that validation of the tool arguments will not be performed, and this will pass all arguments as keyword arguments.

## Dynamic Function tools {#tool-prepare}

Tools can optionally be defined with another function: `prepare`, which is called at each step of a run to
Expand Down Expand Up @@ -624,3 +660,32 @@ def my_flaky_tool(query: str) -> str:
return 'Success!'
```
Raising `ModelRetry` also generates a `RetryPromptPart` containing the exception message, which is sent back to the LLM to guide its next attempt. Both `ValidationError` and `ModelRetry` respect the `retries` setting configured on the `Tool` or `Agent`.

## Use LangChain Tools {#langchain-tools}

If you'd like to use a tool from LangChain's [community tool library](https://python.langchain.com/docs/integrations/tools/) with PydanticAI, you can use the `pydancic_ai.ext.langchain.tool_from_langchain` convenience method. Note that PydanticAI will not validate the arguments in this case -- it's up to the model to provide arguments matching the schema specified by the LangChain tool, and up to the LangChain tool to raise an error if the arguments are invalid.

Here is how you can use it to augment model responses using a LangChain web search tool. This tool will need you to install the `langchain-community` and `duckduckgo-search` dependencies to work properly.

```python {test="skip"}
from langchain_community.tools import DuckDuckGoSearchRun

from pydantic_ai import Agent
from pydantic_ai.ext.langchain import tool_from_langchain

search = DuckDuckGoSearchRun()
search_tool = tool_from_langchain(search)

agent = Agent(
'google-gla:gemini-2.0-flash', # (1)!
tools=[search_tool],
)

result = agent.run_sync('What is the release date of Elden Ring Nightreign?') # (2)!
print(result.output)
#> Elden Ring Nightreign is planned to be released on May 30, 2025.
```


1. While this task is simple Gemini 1.5 didn't want to use the provided tool. Gemini 2.0 is still fast and cheap.
2. The release date of this game is the 30th of May 2025, which was confirmed after the knowledge cutoff for Gemini 2.0 (August 2024).
8 changes: 4 additions & 4 deletions pydantic_ai_slim/pydantic_ai/_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import inspect
from collections.abc import Awaitable
from dataclasses import dataclass
from dataclasses import dataclass, field
from inspect import Parameter, signature
from typing import TYPE_CHECKING, Any, Callable, cast

Expand Down Expand Up @@ -43,9 +43,9 @@ class FunctionSchema:
# if not None, the function takes a single by that name (besides potentially `info`)
takes_ctx: bool
is_async: bool
single_arg_name: str | None
positional_fields: list[str]
var_positional_field: str | None
single_arg_name: str | None = None
positional_fields: list[str] = field(default_factory=list)
var_positional_field: str | None = None

async def call(self, args_dict: dict[str, Any], ctx: RunContext[Any]) -> Any:
args, kwargs = self._call_args(args_dict, ctx)
Expand Down
Empty file.
61 changes: 61 additions & 0 deletions pydantic_ai_slim/pydantic_ai/ext/langchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Any, Protocol

from pydantic.json_schema import JsonSchemaValue

from pydantic_ai.tools import Tool


class LangChainTool(Protocol):
# args are like
# {'dir_path': {'default': '.', 'description': 'Subdirectory to search in.', 'title': 'Dir Path', 'type': 'string'},
# 'pattern': {'description': 'Unix shell regex, where * matches everything.', 'title': 'Pattern', 'type': 'string'}}
@property
def args(self) -> dict[str, JsonSchemaValue]: ...

def get_input_jsonschema(self) -> JsonSchemaValue: ...

@property
def name(self) -> str: ...

@property
def description(self) -> str: ...

def run(self, *args: Any, **kwargs: Any) -> str: ...


__all__ = ('tool_from_langchain',)


def tool_from_langchain(langchain_tool: LangChainTool) -> Tool:
"""Creates a Pydantic tool proxy from a LangChain tool.

Args:
langchain_tool: The LangChain tool to wrap.

Returns:
A Pydantic tool that corresponds to the LangChain tool.
"""
function_name = langchain_tool.name
function_description = langchain_tool.description
inputs = langchain_tool.args.copy()
required = sorted({name for name, detail in inputs.items() if 'default' not in detail})
schema: JsonSchemaValue = langchain_tool.get_input_jsonschema()
if 'additionalProperties' not in schema:
schema['additionalProperties'] = False
if required:
schema['required'] = required

defaults = {name: detail['default'] for name, detail in inputs.items() if 'default' in detail}

# restructures the arguments to match langchain tool run
def proxy(*args: Any, **kwargs: Any) -> str:
assert not args, 'This should always be called with kwargs'
kwargs = defaults | kwargs
return langchain_tool.run(kwargs)

return Tool.from_schema(
function=proxy,
name=function_name,
description=function_description,
json_schema=schema,
)
53 changes: 49 additions & 4 deletions pydantic_ai_slim/pydantic_ai/tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations as _annotations

import asyncio
import dataclasses
import json
from collections.abc import Awaitable, Sequence
Expand All @@ -9,8 +10,8 @@
from opentelemetry.trace import Tracer
from pydantic import ValidationError
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
from pydantic_core import core_schema
from typing_extensions import Concatenate, ParamSpec, TypeAlias, TypeVar
from pydantic_core import SchemaValidator, core_schema
from typing_extensions import Concatenate, ParamSpec, Self, TypeAlias, TypeVar

from . import _function_schema, _utils, messages as _messages
from .exceptions import ModelRetry, UnexpectedModelBehavior
Expand Down Expand Up @@ -63,7 +64,9 @@ class RunContext(Generic[AgentDepsT]):
"""The current step in the run."""

def replace_with(
self, retry: int | None = None, tool_name: str | None | _utils.Unset = _utils.UNSET
self,
retry: int | None = None,
tool_name: str | None | _utils.Unset = _utils.UNSET,
) -> RunContext[AgentDepsT]:
# Create a new `RunContext` a new `retry` value and `tool_name`.
kwargs = {}
Expand Down Expand Up @@ -306,6 +309,45 @@ async def prep_my_tool(
self.require_parameter_descriptions = require_parameter_descriptions
self.strict = strict

@classmethod
def from_schema(
cls,
function: Callable[..., Any],
name: str,
description: str,
json_schema: JsonSchemaValue,
) -> Self:
"""Creates a Pydantic tool from a function and a JSON schema.

Args:
function: The function to call.
This will be called with keywords only, and no validation of
the arguments will be performed.
name: The unique name of the tool that clearly communicates its purpose
description: Used to tell the model how/when/why to use the tool.
You can provide few-shot examples as a part of the description.
json_schema: The schema for the function arguments

Returns:
A Pydantic tool that calls the function
"""
function_schema = _function_schema.FunctionSchema(
function=function,
description=description,
validator=SchemaValidator(schema=core_schema.any_schema()),
json_schema=json_schema,
takes_ctx=False,
is_async=asyncio.iscoroutinefunction(function),
)

return cls(
function,
takes_ctx=False,
name=name,
description=description,
function_schema=function_schema,
)

async def prepare_tool_def(self, ctx: RunContext[AgentDepsT]) -> ToolDefinition | None:
"""Get the tool definition.

Expand All @@ -327,7 +369,10 @@ async def prepare_tool_def(self, ctx: RunContext[AgentDepsT]) -> ToolDefinition
return tool_def

async def run(
self, message: _messages.ToolCallPart, run_context: RunContext[AgentDepsT], tracer: Tracer
self,
message: _messages.ToolCallPart,
run_context: RunContext[AgentDepsT],
tracer: Tracer,
) -> _messages.ToolReturnPart | _messages.RetryPromptPart:
"""Run the tool function asynchronously.

Expand Down
Empty file added tests/ext/__init__.py
Empty file.
Loading