Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4376b96
✨ Add support for OpenAI and Gemini File Search Tools
gorkachea Nov 10, 2025
6cec96f
Fix type checking and formatting issues
gorkachea Nov 11, 2025
4c3fe56
Merge branch 'main' into add-file-search-tools-support
gorkachea Nov 11, 2025
3c8decf
docs: Remove runnable markers from FileSearchTool examples
gorkachea Nov 11, 2025
2343679
Skip tests for file_search documentation examples
gorkachea Nov 11, 2025
666a1bb
Add unit tests for FileSearchTool to improve coverage
gorkachea Nov 11, 2025
7365e20
Update FileSearchTool tests with comprehensive mocking
gorkachea Nov 11, 2025
2ee21c9
Add pragma: no cover to FileSearchTool API-dependent code paths
gorkachea Nov 11, 2025
deef1ec
Remove problematic FileSearchTool tests that access private members
gorkachea Nov 11, 2025
18b4b86
Fix end-of-file formatting
gorkachea Nov 11, 2025
11654ed
Add pragma: no cover to remaining FileSearchTool helper function
gorkachea Nov 11, 2025
1542f5c
Apply ruff formatting
gorkachea Nov 11, 2025
7d683b7
Add pragma: no cover to FileSearchTool status handling line
gorkachea Nov 11, 2025
d8ef07d
Remove incorrect pragma: no cover from anthropic.py line 460
gorkachea Nov 11, 2025
6acbd76
docs: address PR feedback for FileSearchTool documentation
gorkachea Nov 12, 2025
380e25c
clean up FileSearchTool comments
gorkachea Nov 12, 2025
c83f125
remove pragma: no cover from FileSearchTool code
gorkachea Nov 12, 2025
8eba82d
use file_search_store_names for Google file search
gorkachea Nov 12, 2025
b3a8930
fix OpenAI file search to use queries and results fields
gorkachea Nov 12, 2025
19f32f9
add builtin tool call/return parts for Google file search
gorkachea Nov 13, 2025
00ea1ed
Implement FileSearchDict for Google file search and enhance tests
gorkachea Nov 13, 2025
c6ed56c
add unit tests for FileSearchTool parsing logic
gorkachea Nov 13, 2025
9b5bb54
Merge branch 'main' into add-file-search-tools-support
gorkachea Nov 13, 2025
c2765ac
upgrade google-genai SDK to v1.49.0 with file_search support
gorkachea Nov 13, 2025
8286cd7
add integration tests for FileSearchTool
gorkachea Nov 13, 2025
3011e05
add VCR decorators to FileSearchTool integration tests
gorkachea Nov 13, 2025
bc278e8
fix Google FileSearchTool SDK parameters and add VCR decorators
gorkachea Nov 14, 2025
5f694c9
fix type errors in FileSearchTool integration tests
gorkachea Nov 14, 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
60 changes: 60 additions & 0 deletions docs/builtin-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Pydantic AI supports the following built-in tools:
- **[`UrlContextTool`][pydantic_ai.builtin_tools.UrlContextTool]**: Enables agents to pull URL contents into their context
- **[`MemoryTool`][pydantic_ai.builtin_tools.MemoryTool]**: Enables agents to use memory
- **[`MCPServerTool`][pydantic_ai.builtin_tools.MCPServerTool]**: Enables agents to use remote MCP servers with communication handled by the model provider
- **[`FileSearchTool`][pydantic_ai.builtin_tools.FileSearchTool]**: Enables agents to search through uploaded files using vector search (RAG)

These tools are passed to the agent via the `builtin_tools` parameter and are executed by the model provider's infrastructure.

Expand Down Expand Up @@ -566,6 +567,65 @@ _(This example is complete, it can be run "as is")_
| `description` | ✅ | ❌ |
| `headers` | ✅ | ❌ |

## File Search Tool

The [`FileSearchTool`][pydantic_ai.builtin_tools.FileSearchTool] enables your agent to search through uploaded files using vector search, providing a fully managed Retrieval-Augmented Generation (RAG) system. This tool handles file storage, chunking, embedding generation, and context injection into prompts.

### Provider Support

| Provider | Supported | Notes |
|----------|-----------|-------|
| OpenAI Responses | ✅ | Full feature support. Requires files to be uploaded to vector stores via the [OpenAI Files API](https://platform.openai.com/docs/api-reference/files). Vector stores must be created and file IDs added before using the tool. |
| Google (Gemini) | ✅ | Requires files to be uploaded via the [Gemini Files API](https://ai.google.dev/gemini-api/docs/files). Files are automatically deleted after 48 hours. Supports up to 2 GB per file and 20 GB per project. Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| Anthropic | ❌ | Not supported |
| Groq | ❌ | Not supported |
| OpenAI Chat Completions | ❌ | Not supported |
| Bedrock | ❌ | Not supported |
| Mistral | ❌ | Not supported |
| Cohere | ❌ | Not supported |
| HuggingFace | ❌ | Not supported |
| Outlines | ❌ | Not supported |

### Usage

#### OpenAI Responses

With OpenAI, you need to first [upload files to a vector store](https://platform.openai.com/docs/assistants/tools/file-search), then reference the vector store IDs when using the `FileSearchTool`:

```py {title="file_search_openai.py" test="skip"}
from pydantic_ai import Agent, FileSearchTool

agent = Agent(
'openai-responses:gpt-5',
builtin_tools=[FileSearchTool(vector_store_ids=['vs_abc123'])] # (1)
)

result = agent.run_sync('What information is in my documents about pydantic?')
print(result.output)
#> Based on your documents, Pydantic is a data validation library for Python...
```

1. Replace `vs_abc123` with your actual vector store ID from the OpenAI API.

#### Google (Gemini)

With Gemini, you need to first [create a file search store via the Files API](https://ai.google.dev/gemini-api/docs/files), then reference the file search store names:

```py {title="file_search_google.py" test="skip"}
from pydantic_ai import Agent, FileSearchTool

agent = Agent(
'google-gla:gemini-2.5-flash',
builtin_tools=[FileSearchTool(vector_store_ids=['files/abc123'])] # (1)
)

result = agent.run_sync('Summarize the key points from my uploaded documents.')
print(result.output)
#> The documents discuss the following key points: ...
```

1. Replace `files/abc123` with your actual file search store name from the Gemini Files API.

## API Reference

For complete API documentation, see the [API Reference](api/builtin_tools.md).
10 changes: 6 additions & 4 deletions pydantic_ai_slim/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from .builtin_tools import (
CodeExecutionTool,
FileSearchTool,
ImageGenerationTool,
MCPServerTool,
MemoryTool,
Expand Down Expand Up @@ -210,13 +211,14 @@
'ToolsetTool',
'WrapperToolset',
# builtin_tools
'WebSearchTool',
'WebSearchUserLocation',
'UrlContextTool',
'CodeExecutionTool',
'FileSearchTool',
'ImageGenerationTool',
'MemoryTool',
'MCPServerTool',
'MemoryTool',
'UrlContextTool',
'WebSearchTool',
'WebSearchUserLocation',
# output
'ToolOutput',
'NativeOutput',
Expand Down
25 changes: 25 additions & 0 deletions pydantic_ai_slim/pydantic_ai/builtin_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'ImageGenerationTool',
'MemoryTool',
'MCPServerTool',
'FileSearchTool',
)

_BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
Expand Down Expand Up @@ -334,6 +335,30 @@ def unique_id(self) -> str:
return ':'.join([self.kind, self.id])


@dataclass(kw_only=True)
class FileSearchTool(AbstractBuiltinTool):
"""A builtin tool that allows your agent to search through uploaded files using vector search.

This tool provides a fully managed Retrieval-Augmented Generation (RAG) system that handles
file storage, chunking, embedding generation, and context injection into prompts.

Supported by:

* OpenAI Responses
* Google (Gemini)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not vertex AI?

Copy link

@shun-liang shun-liang Nov 12, 2025

Choose a reason for hiding this comment

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

@DouweM Logan Kilpatrick responded on Twitter that Gemini File Search API is not yet available on Vertex AI.

https://x.com/OfficialLoganK/status/1986581779927494837

Copy link
Author

Choose a reason for hiding this comment

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

Thanks @shun-liang for checking! Correct, it's not available on Vertex AI yet according to Logan's response.

"""

vector_store_ids: list[str]
"""List of vector store IDs to search through.

For OpenAI, these are the IDs of vector stores created via the OpenAI API.
For Google, these are file resource names that have been uploaded and processed.
"""

kind: str = 'file_search'
"""The kind of tool."""


def _tool_discriminator(tool_data: dict[str, Any] | AbstractBuiltinTool) -> str:
if isinstance(tool_data, dict):
return tool_data.get('kind', AbstractBuiltinTool.kind)
Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ def _add_builtin_tools(
mcp_server_url_definition_param['authorization_token'] = tool.authorization_token
mcp_servers.append(mcp_server_url_definition_param)
beta_features.append('mcp-client-2025-04-04')
else: # pragma: no cover
else:
raise UserError(
f'`{tool.__class__.__name__}` is not supported by `AnthropicModel`. If it should be, please file an issue.'
)
Expand Down
41 changes: 40 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .. import UnexpectedModelBehavior, _utils, usage
from .._output import OutputObjectDefinition
from .._run_context import RunContext
from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool
from ..builtin_tools import CodeExecutionTool, FileSearchTool, ImageGenerationTool, UrlContextTool, WebSearchTool
from ..exceptions import UserError
from ..messages import (
BinaryContent,
Expand Down Expand Up @@ -61,6 +61,7 @@
ExecutableCode,
ExecutableCodeDict,
FileDataDict,
FileSearchDict,
FinishReason as GoogleFinishReason,
FunctionCallDict,
FunctionCallingConfigDict,
Expand Down Expand Up @@ -91,6 +92,7 @@
'you can use the `google` optional group — `pip install "pydantic-ai-slim[google]"`'
) from _import_error


LatestGoogleModelNames = Literal[
'gemini-2.0-flash',
'gemini-2.0-flash-lite',
Expand Down Expand Up @@ -342,6 +344,9 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[T
tools.append(ToolDict(url_context=UrlContextDict()))
elif isinstance(tool, CodeExecutionTool):
tools.append(ToolDict(code_execution=ToolCodeExecutionDict()))
elif isinstance(tool, FileSearchTool):
file_search_config = FileSearchDict(file_search_store_names=tool.vector_store_ids)
tools.append(ToolDict(file_search=file_search_config))
elif isinstance(tool, ImageGenerationTool): # pragma: no branch
if not self.profile.supports_image_output:
raise UserError(
Expand Down Expand Up @@ -810,6 +815,11 @@ def _process_response_from_parts(
items.append(web_search_call)
items.append(web_search_return)

file_search_call, file_search_return = _map_file_search_grounding_metadata(grounding_metadata, provider_name)
if file_search_call and file_search_return:
items.append(file_search_call)
items.append(file_search_return)

item: ModelResponsePart | None = None
code_execution_tool_call_id: str | None = None
for part in parts:
Expand Down Expand Up @@ -970,3 +980,32 @@ def _map_grounding_metadata(
)
else:
return None, None


def _map_file_search_grounding_metadata(
grounding_metadata: GroundingMetadata | None, provider_name: str
) -> tuple[BuiltinToolCallPart, BuiltinToolReturnPart] | tuple[None, None]:
if grounding_metadata and (retrieval_queries := grounding_metadata.retrieval_queries):
tool_call_id = _utils.generate_tool_call_id()
return (
BuiltinToolCallPart(
provider_name=provider_name,
tool_name=FileSearchTool.kind,
tool_call_id=tool_call_id,
args={'queries': retrieval_queries},
),
BuiltinToolReturnPart(
provider_name=provider_name,
tool_name=FileSearchTool.kind,
tool_call_id=tool_call_id,
content=[
chunk.retrieved_context.model_dump(mode='json')
for chunk in grounding_chunks
if chunk.retrieved_context
]
if (grounding_chunks := grounding_metadata.grounding_chunks)
else None,
),
)
else:
return None, None
82 changes: 78 additions & 4 deletions pydantic_ai_slim/pydantic_ai/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .._run_context import RunContext
from .._thinking_part import split_content_into_text_and_thinking
from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_utc, number_to_datetime
from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, MCPServerTool, WebSearchTool
from ..builtin_tools import CodeExecutionTool, FileSearchTool, ImageGenerationTool, MCPServerTool, WebSearchTool
from ..exceptions import UserError
from ..messages import (
AudioUrl,
Expand Down Expand Up @@ -1070,9 +1070,10 @@ def _process_response( # noqa: C901
elif isinstance(item, responses.response_output_item.LocalShellCall): # pragma: no cover
# Pydantic AI doesn't yet support the `codex-mini-latest` LocalShell built-in tool
pass
elif isinstance(item, responses.ResponseFileSearchToolCall): # pragma: no cover
# Pydantic AI doesn't yet support the FileSearch built-in tool
pass
elif isinstance(item, responses.ResponseFileSearchToolCall):
call_part, return_part = _map_file_search_tool_call(item, self.system)
items.append(call_part)
items.append(return_part)
elif isinstance(item, responses.response_output_item.McpCall):
call_part, return_part = _map_mcp_call(item, self.system)
items.append(call_part)
Expand Down Expand Up @@ -1267,6 +1268,11 @@ def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -
type='approximate', **tool.user_location
)
tools.append(web_search_tool)
elif isinstance(tool, FileSearchTool):
file_search_tool = responses.FileSearchToolParam(
type='file_search', vector_store_ids=tool.vector_store_ids
)
tools.append(file_search_tool)
elif isinstance(tool, CodeExecutionTool):
has_image_generating_tool = True
tools.append({'type': 'code_interpreter', 'container': {'type': 'auto'}})
Expand Down Expand Up @@ -1404,6 +1410,7 @@ async def _map_messages( # noqa: C901
message_item: responses.ResponseOutputMessageParam | None = None
reasoning_item: responses.ResponseReasoningItemParam | None = None
web_search_item: responses.ResponseFunctionWebSearchParam | None = None
file_search_item: responses.ResponseFileSearchToolCallParam | None = None
code_interpreter_item: responses.ResponseCodeInterpreterToolCallParam | None = None
for item in message.parts:
if isinstance(item, TextPart):
Expand Down Expand Up @@ -1473,6 +1480,21 @@ async def _map_messages( # noqa: C901
type='web_search_call',
)
openai_messages.append(web_search_item)
elif (
item.tool_name == FileSearchTool.kind
and item.tool_call_id
and (args := item.args_as_dict())
):
file_search_item = cast(
responses.ResponseFileSearchToolCallParam,
{
'id': item.tool_call_id,
'queries': args.get('queries', []),
'status': 'completed',
'type': 'file_search_call',
},
)
openai_messages.append(file_search_item)
elif item.tool_name == ImageGenerationTool.kind and item.tool_call_id:
# The cast is necessary because of https://github.com/openai/openai-python/issues/2648
image_generation_item = cast(
Expand Down Expand Up @@ -1532,6 +1554,14 @@ async def _map_messages( # noqa: C901
and (status := content.get('status'))
):
web_search_item['status'] = status
elif (
item.tool_name == FileSearchTool.kind
and file_search_item is not None
and isinstance(item.content, dict) # pyright: ignore[reportUnknownMemberType]
and (content := cast(dict[str, Any], item.content)) # pyright: ignore[reportUnknownMemberType]
and (status := content.get('status'))
):
file_search_item['status'] = status
elif item.tool_name == ImageGenerationTool.kind:
# Image generation result does not need to be sent back, just the `id` off of `BuiltinToolCallPart`.
pass
Expand Down Expand Up @@ -1845,6 +1875,11 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
yield self._parts_manager.handle_part(
vendor_part_id=f'{chunk.item.id}-call', part=replace(call_part, args=None)
)
elif isinstance(chunk.item, responses.ResponseFileSearchToolCall):
call_part, _ = _map_file_search_tool_call(chunk.item, self.provider_name)
yield self._parts_manager.handle_part(
vendor_part_id=f'{chunk.item.id}-call', part=replace(call_part, args=None)
)
elif isinstance(chunk.item, responses.ResponseCodeInterpreterToolCall):
call_part, _, _ = _map_code_interpreter_tool_call(chunk.item, self.provider_name)

Expand Down Expand Up @@ -1913,6 +1948,17 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
elif isinstance(chunk.item, responses.ResponseFunctionWebSearch):
call_part, return_part = _map_web_search_tool_call(chunk.item, self.provider_name)

maybe_event = self._parts_manager.handle_tool_call_delta(
vendor_part_id=f'{chunk.item.id}-call',
args=call_part.args,
)
if maybe_event is not None: # pragma: no branch
yield maybe_event

yield self._parts_manager.handle_part(vendor_part_id=f'{chunk.item.id}-return', part=return_part)
elif isinstance(chunk.item, responses.ResponseFileSearchToolCall):
call_part, return_part = _map_file_search_tool_call(chunk.item, self.provider_name)

maybe_event = self._parts_manager.handle_tool_call_delta(
vendor_part_id=f'{chunk.item.id}-call',
args=call_part.args,
Expand Down Expand Up @@ -2216,6 +2262,34 @@ def _map_web_search_tool_call(
)


def _map_file_search_tool_call(
item: responses.ResponseFileSearchToolCall,
provider_name: str,
) -> tuple[BuiltinToolCallPart, BuiltinToolReturnPart]:
args = {'queries': item.queries}

result: dict[str, Any] = {
'status': item.status,
}
if item.results is not None:
result['results'] = [r.model_dump(mode='json') for r in item.results]

return (
BuiltinToolCallPart(
tool_name=FileSearchTool.kind,
tool_call_id=item.id,
args=args,
provider_name=provider_name,
),
BuiltinToolReturnPart(
tool_name=FileSearchTool.kind,
tool_call_id=item.id,
content=result,
provider_name=provider_name,
),
)


def _map_image_generation_tool_call(
item: responses.response_output_item.ImageGenerationCall, provider_name: str
) -> tuple[BuiltinToolCallPart, BuiltinToolReturnPart, FilePart | None]:
Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ logfire = ["logfire[httpx]>=3.14.1"]
openai = ["openai>=1.107.2"]
cohere = ["cohere>=5.18.0; platform_system != 'Emscripten'"]
vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"]
google = ["google-genai>=1.46.0"]
google = ["google-genai>=1.49.0"]
anthropic = ["anthropic>=0.70.0"]
groq = ["groq>=0.25.0"]
mistral = ["mistralai>=1.9.10"]
Expand Down
Loading
Loading