Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
8dc7c17
Regenerate uv.lock with uv 0.9.9 to reduce diff size
gorkachea Nov 15, 2025
8216f31
Remove unit tests for Google FileSearchTool parsing
gorkachea Nov 15, 2025
ffcb21f
Remove unit tests for OpenAI FileSearchTool parsing
gorkachea Nov 15, 2025
bc3ac7a
Refactor Google FileSearchTool tests to match built-in tool pattern
gorkachea Nov 15, 2025
977ab53
Refactor OpenAI FileSearchTool tests to match built-in tool pattern
gorkachea Nov 15, 2025
68bafb6
Merge main into add-file-search-tools-support
gorkachea Nov 15, 2025
29f8da0
Merge main into add-file-search-tools-support
gorkachea Nov 15, 2025
eef4526
Add cassettes
DouweM Nov 20, 2025
8cc3d60
Merge branch 'main' into add-file-search-tools-support
DouweM Nov 20, 2025
b77d857
Rename vector_store_ids to file_store_ids and add Vertex AI to provid…
gorkachea Nov 22, 2025
db475c6
Add file upload examples for OpenAI and Google FileSearchTool
gorkachea Nov 23, 2025
fd62e29
Add openai_include_file_search_results setting
gorkachea Nov 23, 2025
13abf31
Fix Google non-streaming file search to extract retrievedContext
gorkachea Nov 23, 2025
129dacd
Fix Google streaming file search parsing
gorkachea Nov 23, 2025
8d3f359
Merge latest from main
gorkachea Nov 23, 2025
065c711
Revert boto3 version bump to match main
gorkachea Nov 23, 2025
50ad873
Fix CI typecheck and formatting errors
gorkachea Nov 23, 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
83 changes: 83 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,88 @@ _(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). To include search results on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] available via [`ModelResponse.builtin_tool_calls`][pydantic_ai.messages.ModelResponse.builtin_tool_calls], enable the [`OpenAIResponsesModelSettings.openai_include_file_search_results`][pydantic_ai.models.openai.OpenAIResponsesModelSettings.openai_include_file_search_results] [model setting](agents.md#model-run-settings). |
| 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. |
|| Google (Vertex AI) | ❌ | Not supported |
| 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_upload.py" test="skip"}
from pydantic_ai import Agent, FileSearchTool
from pydantic_ai.models.openai import OpenAIResponsesModel

model = OpenAIResponsesModel('gpt-5')

with open('my_document.txt', 'rb') as f:
file = await model.client.files.create(file=f, purpose='assistants')

vector_store = await model.client.vector_stores.create(name='my-docs')
await model.client.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=file.id
)

agent = Agent(
model,
builtin_tools=[FileSearchTool(file_store_ids=[vector_store.id])]
)

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

#### 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_upload.py" test="skip"}
from pydantic_ai import Agent, FileSearchTool
from pydantic_ai.models.google import GoogleModel

model = GoogleModel('gemini-2.5-flash')

store = await model.client.aio.file_search_stores.create(
config={'display_name': 'my-docs'}
)

with open('my_document.txt', 'rb') as f:
await model.client.aio.file_search_stores.upload_to_file_search_store(
file_search_store_name=store.name,
file=f,
config={'mime_type': 'text/plain'}
)

agent = Agent(
model,
builtin_tools=[FileSearchTool(file_store_ids=[store.name])]
)

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

## API Reference

For complete API documentation, see the [API Reference](api/builtin_tools.md).
2 changes: 1 addition & 1 deletion docs/models/openai.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ model_settings = OpenAIResponsesModelSettings(
openai_builtin_tools=[
FileSearchToolParam(
type='file_search',
vector_store_ids=['your-history-book-vector-store-id']
file_store_ids=['your-history-book-vector-store-id']
)
],
)
Expand Down
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 @@ -214,13 +215,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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@gorkachea I just asked our contact at Google and they say:

Vertex AI will not add file_search as a tool as they support vertex_ai_search or external Vector DBs

It'd be nice to eventually support vertex_ai_search as well, but let's not do that in this PR.

I do want to make it more explicit that Vertex is not support though. So let's add a Google (Vertex AI) row to the "Provider Support" table in the doc explaining it's not supported (as we do for the other providers).

Copy link
Collaborator

Choose a reason for hiding this comment

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

This one is still to do!

Copy link
Author

Choose a reason for hiding this comment

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

Done! Added Google (Vertex AI) row to the provider support table.

"""

file_store_ids: list[str]
"""List of file store IDs to search through.

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

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 @@ -540,7 +540,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
102 changes: 97 additions & 5 deletions 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 ModelAPIError, ModelHTTPError, UserError
from ..messages import (
BinaryContent,
Expand Down Expand Up @@ -63,6 +63,7 @@
ExecutableCode,
ExecutableCodeDict,
FileDataDict,
FileSearchDict,
FinishReason as GoogleFinishReason,
FunctionCallDict,
FunctionCallingConfigDict,
Expand Down Expand Up @@ -93,6 +94,7 @@
'you can use the `google` optional group — `pip install "pydantic-ai-slim[google]"`'
) from _import_error


LatestGoogleModelNames = Literal[
'gemini-flash-latest',
'gemini-flash-lite-latest',
Expand Down Expand Up @@ -350,6 +352,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.file_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 @@ -652,6 +657,7 @@ class GeminiStreamedResponse(StreamedResponse):
_timestamp: datetime
_provider_name: str
_provider_url: str
_file_search_tool_call_id: str | None = field(default=None, init=False)

async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # noqa: C901
code_execution_tool_call_id: str | None = None
Expand Down Expand Up @@ -697,6 +703,26 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
continue # pragma: no cover

for part in parts:
if self._file_search_tool_call_id and candidate.grounding_metadata:
grounding_chunks = candidate.grounding_metadata.grounding_chunks
if grounding_chunks:
retrieved_contexts = [
chunk.retrieved_context.model_dump(mode='json')
for chunk in grounding_chunks
if chunk.retrieved_context
]
if retrieved_contexts:
yield self._parts_manager.handle_part(
vendor_part_id=uuid4(),
part=BuiltinToolReturnPart(
provider_name=self.provider_name,
tool_name=FileSearchTool.kind,
tool_call_id=self._file_search_tool_call_id,
content={'retrieved_contexts': retrieved_contexts},
),
)
self._file_search_tool_call_id = None

provider_details: dict[str, Any] | None = None
if part.thought_signature:
# Per https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#thought-signatures:
Expand Down Expand Up @@ -739,10 +765,27 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
part=FilePart(content=BinaryContent.narrow_type(content), provider_details=provider_details),
)
elif part.executable_code is not None:
code_execution_tool_call_id = _utils.generate_tool_call_id()
part = _map_executable_code(part.executable_code, self.provider_name, code_execution_tool_call_id)
part.provider_details = provider_details
yield self._parts_manager.handle_part(vendor_part_id=uuid4(), part=part)
code = part.executable_code.code
if code and (file_search_query := _extract_file_search_query(code)):
self._file_search_tool_call_id = _utils.generate_tool_call_id()
part_obj = BuiltinToolCallPart(
provider_name=self.provider_name,
tool_name=FileSearchTool.kind,
tool_call_id=self._file_search_tool_call_id,
args={'query': file_search_query},
)
part_obj.provider_details = provider_details
yield self._parts_manager.handle_part(
vendor_part_id=uuid4(),
part=part_obj,
)
else:
code_execution_tool_call_id = _utils.generate_tool_call_id()
part_obj = _map_executable_code(
part.executable_code, self.provider_name, code_execution_tool_call_id
)
part_obj.provider_details = provider_details
yield self._parts_manager.handle_part(vendor_part_id=uuid4(), part=part_obj)
elif part.code_execution_result is not None:
assert code_execution_tool_call_id is not None
part = _map_code_execution_result(
Expand Down Expand Up @@ -856,6 +899,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 @@ -1007,3 +1055,47 @@ 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 not grounding_metadata or not (grounding_chunks := grounding_metadata.grounding_chunks):
return None, None

retrieved_contexts = [
chunk.retrieved_context.model_dump(mode='json') for chunk in grounding_chunks if chunk.retrieved_context
]

if not retrieved_contexts:
return None, None

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={},
),
BuiltinToolReturnPart(
provider_name=provider_name,
tool_name=FileSearchTool.kind,
tool_call_id=tool_call_id,
content={'retrieved_contexts': retrieved_contexts},
),
)


def _extract_file_search_query(code: str) -> str | None:
"""Extract the query from file_search.query() executable code.

Example: 'print(file_search.query(query="what is the capital of France?"))'
Returns: 'what is the capital of France?'
"""
import re

match = re.search(r'file_search\.query\(query=(["\'])(.+?)\1\)', code)
if match:
return match.group(2)
return None
Loading
Loading