diff --git a/docs/builtin-tools.md b/docs/builtin-tools.md index 1e4bdfd279..b03b5a7ec1 100644 --- a/docs/builtin-tools.md +++ b/docs/builtin-tools.md @@ -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. @@ -566,6 +567,98 @@ _(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"} +import asyncio + +from pydantic_ai import Agent, FileSearchTool +from pydantic_ai.models.openai import OpenAIResponsesModel + +async def main(): + 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... + +asyncio.run(main()) +``` + +#### 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"} +import asyncio + +from pydantic_ai import Agent, FileSearchTool +from pydantic_ai.models.google import GoogleModel + +async def main(): + 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: ... + +asyncio.run(main()) +``` + ## API Reference For complete API documentation, see the [API Reference](api/builtin_tools.md). diff --git a/docs/models/openai.md b/docs/models/openai.md index 2eae27bd03..a2849dbc45 100644 --- a/docs/models/openai.md +++ b/docs/models/openai.md @@ -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'] ) ], ) diff --git a/pydantic_ai_slim/pydantic_ai/__init__.py b/pydantic_ai_slim/pydantic_ai/__init__.py index f33a0ad3ec..bc1c9ee9fd 100644 --- a/pydantic_ai_slim/pydantic_ai/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/__init__.py @@ -11,6 +11,7 @@ ) from .builtin_tools import ( CodeExecutionTool, + FileSearchTool, ImageGenerationTool, MCPServerTool, MemoryTool, @@ -214,13 +215,14 @@ 'ToolsetTool', 'WrapperToolset', # builtin_tools - 'WebSearchTool', - 'WebSearchUserLocation', - 'UrlContextTool', 'CodeExecutionTool', + 'FileSearchTool', 'ImageGenerationTool', - 'MemoryTool', 'MCPServerTool', + 'MemoryTool', + 'UrlContextTool', + 'WebSearchTool', + 'WebSearchUserLocation', # output 'ToolOutput', 'NativeOutput', diff --git a/pydantic_ai_slim/pydantic_ai/builtin_tools.py b/pydantic_ai_slim/pydantic_ai/builtin_tools.py index 5559b3124a..03b6c8b31f 100644 --- a/pydantic_ai_slim/pydantic_ai/builtin_tools.py +++ b/pydantic_ai_slim/pydantic_ai/builtin_tools.py @@ -17,6 +17,7 @@ 'ImageGenerationTool', 'MemoryTool', 'MCPServerTool', + 'FileSearchTool', ) _BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {} @@ -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) + """ + + 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) diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index 342c141b9d..333bf145d3 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -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.' ) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 750b6e15b0..64f8f15f5f 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -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, @@ -63,6 +63,7 @@ ExecutableCode, ExecutableCodeDict, FileDataDict, + FileSearchDict, FinishReason as GoogleFinishReason, FunctionCallDict, FunctionCallingConfigDict, @@ -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', @@ -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( @@ -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 @@ -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: @@ -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( @@ -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: @@ -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 diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 6db3742320..b33e2a1e33 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -19,7 +19,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, @@ -275,6 +275,12 @@ class OpenAIResponsesModelSettings(OpenAIChatModelSettings, total=False): Corresponds to the `web_search_call.action.sources` value of the `include` parameter in the Responses API. """ + openai_include_file_search_results: bool + """Whether to include the file search results in the response. + + Corresponds to the `file_search_call.results` value of the `include` parameter in the Responses API. + """ + @dataclass(init=False) class OpenAIChatModel(Model): @@ -1192,9 +1198,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) @@ -1323,6 +1330,8 @@ async def _responses_create( include.append('code_interpreter_call.outputs') if model_settings.get('openai_include_web_search_sources'): include.append('web_search_call.action.sources') + if model_settings.get('openai_include_file_search_results'): + include.append('file_search_call.results') try: extra_headers = model_settings.get('extra_headers', {}) @@ -1391,6 +1400,12 @@ 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 = cast( + responses.FileSearchToolParam, + {'type': 'file_search', 'file_store_ids': tool.file_store_ids}, + ) + tools.append(file_search_tool) elif isinstance(tool, CodeExecutionTool): has_image_generating_tool = True tools.append({'type': 'code_interpreter', 'container': {'type': 'auto'}}) @@ -1528,6 +1543,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): @@ -1597,6 +1613,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( @@ -1656,6 +1687,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 @@ -2029,6 +2068,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) @@ -2097,6 +2141,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, @@ -2246,6 +2301,15 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: elif isinstance(chunk, responses.ResponseMcpCallCompletedEvent): pass # there's nothing we need to do here + elif isinstance(chunk, responses.ResponseFileSearchCallCompletedEvent): + pass # there's nothing we need to do here + + elif isinstance(chunk, responses.ResponseFileSearchCallSearchingEvent): + pass # there's nothing we need to do here + + elif isinstance(chunk, responses.ResponseFileSearchCallInProgressEvent): + pass # there's nothing we need to do here + else: # pragma: no cover warnings.warn( f'Handling of this event type is not yet implemented. Please report on our GitHub: {chunk}', @@ -2426,6 +2490,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]: diff --git a/tests/json_body_serializer.py b/tests/json_body_serializer.py index a0cadd3259..172d231689 100644 --- a/tests/json_body_serializer.py +++ b/tests/json_body_serializer.py @@ -15,6 +15,7 @@ FILTERED_HEADER_PREFIXES = ['anthropic-', 'cf-', 'x-'] FILTERED_HEADERS = {'authorization', 'date', 'request-id', 'server', 'user-agent', 'via', 'set-cookie', 'api-key'} +ALLOWED_HEADERS = {'x-goog-upload-url', 'x-goog-upload-status'} # required for test_google_model_file_search_tool class LiteralDumper(Dumper): @@ -55,7 +56,9 @@ def serialize(cassette_dict: Any): # pragma: lax no cover headers = {k: v for k, v in headers.items() if k not in FILTERED_HEADERS} # filter headers by prefix headers = { - k: v for k, v in headers.items() if not any(k.startswith(prefix) for prefix in FILTERED_HEADER_PREFIXES) + k: v + for k, v in headers.items() + if not any(k.startswith(prefix) for prefix in FILTERED_HEADER_PREFIXES) or k in ALLOWED_HEADERS } # update headers on source object data['headers'] = headers diff --git a/tests/models/cassettes/test_google/test_google_model_file_search_tool.yaml b/tests/models/cassettes/test_google/test_google_model_file_search_tool.yaml new file mode 100644 index 0000000000..19d3f3ed37 --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_model_file_search_tool.yaml @@ -0,0 +1,352 @@ +interactions: +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '41' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + displayName: test-file-search-store + uri: https://generativelanguage.googleapis.com/v1beta/fileSearchStores + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '203' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=373 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + createTime: '2025-11-20T23:37:08.933211Z' + displayName: test-file-search-store + name: fileSearchStores/testfilesearchstore-s6zmrh92ulpr + updateTime: '2025-11-20T23:37:08.933211Z' + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '26' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + mimeType: text/plain + uri: https://generativelanguage.googleapis.com/upload/v1beta/fileSearchStores/testfilesearchstore-s6zmrh92ulpr:uploadToFileSearchStore + response: + body: + string: '' + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '0' + content-type: + - text/plain; charset=utf-8 + x-goog-upload-status: + - active + x-goog-upload-url: + - https://generativelanguage.googleapis.com/upload/v1beta/fileSearchStores/testfilesearchstore-s6zmrh92ulpr:uploadToFileSearchStore?upload_id=AOCedOHMyp8-WH0roPvEsby85i78ZlUxHR4ZVhKtN6xXYFFUkHedk2H5w7cIOz4M9p4LeWPKM46woeaM3jNrhq6vfgJmt4ds3RnrIiO5LEsa3g&upload_protocol=resumable + status: + code: 200 + message: OK +- request: + body: Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris. + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '79' + host: + - generativelanguage.googleapis.com + method: POST + uri: https://generativelanguage.googleapis.com/upload/v1beta/fileSearchStores/testfilesearchstore-s6zmrh92ulpr:uploadToFileSearchStore?upload_id=AOCedOHMyp8-WH0roPvEsby85i78ZlUxHR4ZVhKtN6xXYFFUkHedk2H5w7cIOz4M9p4LeWPKM46woeaM3jNrhq6vfgJmt4ds3RnrIiO5LEsa3g&upload_protocol=resumable + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '463' + content-type: + - application/json; charset=UTF-8 + vary: + - Origin + - X-Origin + - Referer + x-goog-upload-status: + - final + parsed_body: + name: fileSearchStores/testfilesearchstore-s6zmrh92ulpr/upload/operations/k742l26km1p8-jjheny21pwon + response: + '@type': type.googleapis.com/google.ai.generativelanguage.v1main.UploadToFileSearchStoreResponse + documentName: fileSearchStores/testfilesearchstore-s6zmrh92ulpr/documents/k742l26km1p8-jjheny21pwon + mimeType: text/plain + parent: fileSearchStores/testfilesearchstore-s6zmrh92ulpr + sizeBytes: '79' + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '344' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What is the capital of France? + role: user + generationConfig: + responseModalities: + - TEXT + systemInstruction: + parts: + - text: You are a helpful assistant. + role: user + tools: + - fileSearch: + file_search_store_names: + - fileSearchStores/testfilesearchstore-s6zmrh92ulpr + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '2105' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=11428 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - text: |- + The capital of France is Paris. + + Paris is a major global center for art, fashion, gastronomy, and culture. A famous landmark in the city is the Eiffel Tower. The city is also known for its 19th-century cityscape, which is crisscrossed by wide boulevards and the River Seine. + role: model + finishReason: STOP + groundingMetadata: + groundingChunks: + - retrievedContext: + fileSearchStore: fileSearchStores/testfilesearchstore-s6zmrh92ulpr + text: Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris. + - retrievedContext: + fileSearchStore: fileSearchStores/testfilesearchstore-s6zmrh92ulpr + text: Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris. + groundingSupports: + - groundingChunkIndices: + - 1 + segment: + endIndex: 30 + text: The capital of France is Paris + - groundingChunkIndices: + - 1 + segment: + endIndex: 156 + startIndex: 107 + text: A famous landmark in the city is the Eiffel Tower + index: 0 + modelVersion: gemini-2.5-pro + responseId: NKYfaZHSO4udqtsP5YSmuAs + usageMetadata: + candidatesTokenCount: 100 + promptTokenCount: 15 + promptTokensDetails: + - modality: TEXT + tokenCount: 15 + thoughtsTokenCount: 879 + toolUsePromptTokenCount: 1485 + toolUsePromptTokensDetails: + - modality: TEXT + tokenCount: 1485 + totalTokenCount: 2479 + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '738' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What is the capital of France? + role: user + - parts: + - text: |- + The capital of France is Paris. + + Paris is a major global center for art, fashion, gastronomy, and culture. A famous landmark in the city is the Eiffel Tower. The city is also known for its 19th-century cityscape, which is crisscrossed by wide boulevards and the River Seine. + role: model + - parts: + - text: Tell me about the Eiffel Tower. + role: user + generationConfig: + responseModalities: + - TEXT + systemInstruction: + parts: + - text: You are a helpful assistant. + role: user + tools: + - fileSearch: + file_search_store_names: + - fileSearchStores/testfilesearchstore-s6zmrh92ulpr + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '2782' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=8799 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - text: |- + The Eiffel Tower is a famous landmark located in Paris, the capital of France. It is one of the most recognizable structures in the world. + + Here are some key facts about the Eiffel Tower: + + * **Construction:** It was designed by the engineer Gustave Eiffel, and his company built it between 1887 and 1889. It was created as the entrance arch for the 1889 Exposition Universelle (World's Fair), which celebrated the 100th anniversary of the French Revolution. + * **Design and Material:** The tower is made of wrought iron and is a masterpiece of structural engineering. Its open-lattice design was innovative for its time and was chosen for its wind resistance and stability. + * **Height:** The Eiffel Tower stands at a height of 330 meters (1,083 feet), including its antennas. For about 41 years, it held the title of the tallest man-made structure in the world until the Chrysler Building in New York City was completed in 1930. + * **Visiting:** The tower has three levels for visitors. The first two levels can be reached by stairs or elevators and feature restaurants and shops. The top level, accessible only by elevator, offers panoramic views of Paris. + * **Cultural Impact:** Initially criticized by some of France's leading artists and intellectuals for its design, the Eiffel Tower has since become a global cultural icon of France and a symbol of Paris itself. It is one of the most visited paid monuments in the world. + role: model + finishReason: STOP + groundingMetadata: + groundingChunks: + - retrievedContext: + fileSearchStore: fileSearchStores/testfilesearchstore-s6zmrh92ulpr + text: Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris. + groundingSupports: + - groundingChunkIndices: + - 0 + segment: + endIndex: 78 + text: The Eiffel Tower is a famous landmark located in Paris, the capital of France. + index: 0 + modelVersion: gemini-2.5-pro + responseId: PqYfadjYC8eHqtsPicvToAs + usageMetadata: + candidatesTokenCount: 339 + promptTokenCount: 88 + promptTokensDetails: + - modality: TEXT + tokenCount: 88 + thoughtsTokenCount: 167 + toolUsePromptTokenCount: 270 + toolUsePromptTokensDetails: + - modality: TEXT + tokenCount: 270 + totalTokenCount: 864 + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: DELETE + uri: https://generativelanguage.googleapis.com/v1beta/fileSearchStores/testfilesearchstore-s6zmrh92ulpr?force=True + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '3' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=528 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_google/test_google_model_file_search_tool_stream.yaml b/tests/models/cassettes/test_google/test_google_model_file_search_tool_stream.yaml new file mode 100644 index 0000000000..eac2146f75 --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_model_file_search_tool_stream.yaml @@ -0,0 +1,234 @@ +interactions: +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '42' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + displayName: test-file-search-stream + uri: https://generativelanguage.googleapis.com/v1beta/fileSearchStores + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '205' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=292 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + createTime: '2025-11-20T23:37:35.563554Z' + displayName: test-file-search-stream + name: fileSearchStores/testfilesearchstream-df5lsen5e6i5 + updateTime: '2025-11-20T23:37:35.563554Z' + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '26' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + mimeType: text/plain + uri: https://generativelanguage.googleapis.com/upload/v1beta/fileSearchStores/testfilesearchstream-df5lsen5e6i5:uploadToFileSearchStore + response: + body: + string: '' + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '0' + content-type: + - text/plain; charset=utf-8 + x-goog-upload-status: + - active + x-goog-upload-url: + - https://generativelanguage.googleapis.com/upload/v1beta/fileSearchStores/testfilesearchstream-df5lsen5e6i5:uploadToFileSearchStore?upload_id=AOCedOGNfObKqPepswPfad1VURMafRGrNEQVrJiepVNtwNXCJvYNNAtJEWKxzRzqRKDBLF2WSSRcuRvj7e4LVEIe4A92ZydvIolV6wRE7-qTClo&upload_protocol=resumable + status: + code: 200 + message: OK +- request: + body: Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris. + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '79' + host: + - generativelanguage.googleapis.com + method: POST + uri: https://generativelanguage.googleapis.com/upload/v1beta/fileSearchStores/testfilesearchstream-df5lsen5e6i5:uploadToFileSearchStore?upload_id=AOCedOGNfObKqPepswPfad1VURMafRGrNEQVrJiepVNtwNXCJvYNNAtJEWKxzRzqRKDBLF2WSSRcuRvj7e4LVEIe4A92ZydvIolV6wRE7-qTClo&upload_protocol=resumable + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '466' + content-type: + - application/json; charset=UTF-8 + vary: + - Origin + - X-Origin + - Referer + x-goog-upload-status: + - final + parsed_body: + name: fileSearchStores/testfilesearchstream-df5lsen5e6i5/upload/operations/iifcpg88rbed-d4ggi3qbmddb + response: + '@type': type.googleapis.com/google.ai.generativelanguage.v1main.UploadToFileSearchStoreResponse + documentName: fileSearchStores/testfilesearchstream-df5lsen5e6i5/documents/iifcpg88rbed-d4ggi3qbmddb + mimeType: text/plain + parent: fileSearchStores/testfilesearchstream-df5lsen5e6i5 + sizeBytes: '79' + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '345' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What is the capital of France? + role: user + generationConfig: + responseModalities: + - TEXT + systemInstruction: + parts: + - text: You are a helpful assistant. + role: user + tools: + - fileSearch: + file_search_store_names: + - fileSearchStores/testfilesearchstream-df5lsen5e6i5 + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"executableCode\": {\"language\": \"PYTHON\",\"code\": + \" print(file_search.query(query=\\\"what is the capital of France?\\\"))\\n \"},\"thought\": true}],\"role\": + \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 15,\"candidatesTokenCount\": 18,\"totalTokenCount\": + 212,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"thoughtsTokenCount\": 179},\"modelVersion\": + \"gemini-2.5-pro\",\"responseId\": \"RaYfafPuAc63qtsP-bOCyQw\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": + [{\"executableCode\": {\"language\": \"PYTHON\",\"code\": \"print(file_search.query(query=\\\"what is the capital + of France?\\\"))\"}}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0,\"groundingMetadata\": {\"groundingChunks\": + [{\"retrievedContext\": {\"text\": \"Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.\",\"fileSearchStore\": + \"fileSearchStores/testfilesearchstream-df5lsen5e6i5\"}}]}}],\"usageMetadata\": {\"promptTokenCount\": 15,\"candidatesTokenCount\": + 36,\"totalTokenCount\": 500,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"toolUsePromptTokenCount\": + 238,\"toolUsePromptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 238}],\"thoughtsTokenCount\": 211},\"modelVersion\": + \"gemini-2.5-pro\",\"responseId\": \"RaYfafPuAc63qtsP-bOCyQw\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": + [{\"text\": \"The capital of France\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": + 15,\"candidatesTokenCount\": 40,\"totalTokenCount\": 792,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 15}],\"toolUsePromptTokenCount\": 526,\"toolUsePromptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 526}],\"thoughtsTokenCount\": + 211},\"modelVersion\": \"gemini-2.5-pro\",\"responseId\": \"RaYfafPuAc63qtsP-bOCyQw\"}\r\n\r\ndata: {\"candidates\": + [{\"content\": {\"parts\": [{\"text\": \" is Paris. A famous landmark in Paris is the Eiffel\"}],\"role\": \"model\"},\"index\": + 0}],\"usageMetadata\": {\"promptTokenCount\": 15,\"candidatesTokenCount\": 51,\"totalTokenCount\": 803,\"promptTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"toolUsePromptTokenCount\": 526,\"toolUsePromptTokensDetails\": [{\"modality\": + \"TEXT\",\"tokenCount\": 526}],\"thoughtsTokenCount\": 211},\"modelVersion\": \"gemini-2.5-pro\",\"responseId\": \"RaYfafPuAc63qtsP-bOCyQw\"}\r\n\r\ndata: + {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \" Tower.\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": + 0,\"groundingMetadata\": {\"groundingChunks\": [{\"retrievedContext\": {\"text\": \"Paris is the capital of France. + The Eiffel Tower is a famous landmark in Paris.\",\"fileSearchStore\": \"fileSearchStores/testfilesearchstream-df5lsen5e6i5\"}}],\"groundingSupports\": + [{\"segment\": {\"endIndex\": 31,\"text\": \"The capital of France is Paris.\"},\"groundingChunkIndices\": [1]},{\"segment\": + {\"startIndex\": 32,\"endIndex\": 79,\"text\": \"A famous landmark in Paris is the Eiffel Tower.\"},\"groundingChunkIndices\": + [1]}]}}],\"usageMetadata\": {\"promptTokenCount\": 15,\"candidatesTokenCount\": 53,\"totalTokenCount\": 805,\"promptTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"toolUsePromptTokenCount\": 526,\"toolUsePromptTokensDetails\": [{\"modality\": + \"TEXT\",\"tokenCount\": 526}],\"thoughtsTokenCount\": 211},\"modelVersion\": \"gemini-2.5-pro\",\"responseId\": \"RaYfafPuAc63qtsP-bOCyQw\"}\r\n\r\n" + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-disposition: + - attachment + content-type: + - text/event-stream + server-timing: + - gfet4t7; dur=2325 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: DELETE + uri: https://generativelanguage.googleapis.com/v1beta/fileSearchStores/testfilesearchstream-df5lsen5e6i5?force=True + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '3' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=475 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_openai_responses/test_openai_responses_model_file_search_tool.yaml b/tests/models/cassettes/test_openai_responses/test_openai_responses_model_file_search_tool.yaml new file mode 100644 index 0000000000..64f9574e9c --- /dev/null +++ b/tests/models/cassettes/test_openai_responses/test_openai_responses_model_file_search_tool.yaml @@ -0,0 +1,542 @@ +interactions: +- request: + body: "--ebe5e3e9ce6b7300181a8dbffc147456\r\nContent-Disposition: form-data; name=\"purpose\"\r\n\r\nassistants\r\n--ebe5e3e9ce6b7300181a8dbffc147456\r\nContent-Disposition: + form-data; name=\"file\"; filename=\"tmppqk8yolf.txt\"\r\nContent-Type: text/plain\r\n\r\nParis is the capital of France. + It is known for the Eiffel Tower.\r\n--ebe5e3e9ce6b7300181a8dbffc147456--\r\n" + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '340' + content-type: + - multipart/form-data; boundary=ebe5e3e9ce6b7300181a8dbffc147456 + host: + - api.openai.com + method: POST + uri: https://api.openai.com/v1/files + response: + headers: + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '238' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '299' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + bytes: 65 + created_at: 1763679014 + expires_at: null + filename: tmppqk8yolf.txt + id: file-5SYDnrNwrDK7shmFdZLZYJ + object: file + purpose: assistants + status: processed + status_details: null + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '27' + content-type: + - application/json + cookie: + - __cf_bm=DsC.inynDkGDmfnXv4NNUqQLgIq84x95145.YA_FzXk-1763679014-1.0.1.1-Q_t53YBIxNllTEy7TRZ.UkqkFg3eq4iKBDhwwKVuVrFgLVuK1_1agn1NnbUsMNT0E9s3tRhKzmcJGOLy4e9NN7IFaWwdDAbCK7W0RaC7960; + _cfuvid=UicT82uY5GgDJp4DnxkfgSxnSIQ6nG3sk1pdJ.Fwr4M-1763679014585-0.0.1.1-604800000 + host: + - api.openai.com + openai-beta: + - assistants=v2 + method: POST + parsed_body: + name: test-file-search + uri: https://api.openai.com/v1/vector_stores + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '418' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '572' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + created_at: 1763679015 + description: null + expires_after: null + expires_at: null + file_counts: + cancelled: 0 + completed: 0 + failed: 0 + in_progress: 0 + total: 0 + id: vs_691f9b2716c08191bd8a0d05c5008de1 + last_active_at: 1763679015 + metadata: {} + name: test-file-search + object: vector_store + status: completed + usage_bytes: 0 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '41' + content-type: + - application/json + cookie: + - __cf_bm=DsC.inynDkGDmfnXv4NNUqQLgIq84x95145.YA_FzXk-1763679014-1.0.1.1-Q_t53YBIxNllTEy7TRZ.UkqkFg3eq4iKBDhwwKVuVrFgLVuK1_1agn1NnbUsMNT0E9s3tRhKzmcJGOLy4e9NN7IFaWwdDAbCK7W0RaC7960; + _cfuvid=UicT82uY5GgDJp4DnxkfgSxnSIQ6nG3sk1pdJ.Fwr4M-1763679014585-0.0.1.1-604800000 + host: + - api.openai.com + openai-beta: + - assistants=v2 + method: POST + parsed_body: + file_id: file-5SYDnrNwrDK7shmFdZLZYJ + uri: https://api.openai.com/v1/vector_stores/vs_691f9b2716c08191bd8a0d05c5008de1/files + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '958' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + attributes: {} + chunking_strategy: + static: + chunk_overlap_tokens: 400 + max_chunk_size_tokens: 800 + type: static + created_at: 1763679016 + id: file-5SYDnrNwrDK7shmFdZLZYJ + last_error: null + object: vector_store.file + status: in_progress + usage_bytes: 0 + vector_store_id: vs_691f9b2716c08191bd8a0d05c5008de1 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '261' + content-type: + - application/json + cookie: + - __cf_bm=DsC.inynDkGDmfnXv4NNUqQLgIq84x95145.YA_FzXk-1763679014-1.0.1.1-Q_t53YBIxNllTEy7TRZ.UkqkFg3eq4iKBDhwwKVuVrFgLVuK1_1agn1NnbUsMNT0E9s3tRhKzmcJGOLy4e9NN7IFaWwdDAbCK7W0RaC7960; + _cfuvid=UicT82uY5GgDJp4DnxkfgSxnSIQ6nG3sk1pdJ.Fwr4M-1763679014585-0.0.1.1-604800000 + host: + - api.openai.com + method: POST + parsed_body: + input: + - content: What is the capital of France? + role: user + instructions: You are a helpful assistant. + model: gpt-4o + stream: false + tool_choice: auto + tools: + - type: file_search + file_store_ids: + - vs_691f9b2716c08191bd8a0d05c5008de1 + uri: https://api.openai.com/v1/responses + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '2004' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '2130' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + background: false + billing: + payer: developer + created_at: 1763679019 + error: null + id: resp_0807d60d5445886300691f9b2bc4148193b8e7221b12a9136b + incomplete_details: null + instructions: You are a helpful assistant. + max_output_tokens: null + max_tool_calls: null + metadata: {} + model: gpt-4o-2024-08-06 + object: response + output: + - id: fs_0807d60d5445886300691f9b2cc3f08193a11ab9cf8f735574 + queries: + - What is the capital of France? + results: null + status: completed + type: file_search_call + - content: + - annotations: [] + logprobs: [] + text: The capital of France is Paris. + type: output_text + id: msg_0807d60d5445886300691f9b2d7b708193a2e0789bcc41ef5f + role: assistant + status: completed + type: message + parallel_tool_calls: true + previous_response_id: null + prompt_cache_key: null + prompt_cache_retention: null + reasoning: + effort: null + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: auto + tools: + - filters: null + max_num_results: 20 + ranking_options: + ranker: auto + score_threshold: 0.0 + type: file_search + file_store_ids: + - vs_691f9b2716c08191bd8a0d05c5008de1 + top_logprobs: 0 + top_p: 1.0 + truncation: disabled + usage: + input_tokens: 870 + input_tokens_details: + cached_tokens: 0 + output_tokens: 30 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 900 + user: null + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '386' + content-type: + - application/json + cookie: + - __cf_bm=E5f.uTRtWuVMWvVfIOlPnmO1VssA9e0wxm.ib6Am5PY-1763679021-1.0.1.1-Jy7.32_vn8tOGfOexRT6Q3s2w5kUE.99qJmB7KDtVCKlqpFuTCEJGwSFII_dqYG7CRuR_VO6mRShith0PiLMFsSsFMZHKSYYoxxSWDVGz3o; + _cfuvid=Qk6c2yE2yjyTQsPJH9hhq6EAYojWx242WTVJbNnFLLI-1763679021903-0.0.1.1-604800000 + host: + - api.openai.com + method: POST + parsed_body: + input: + - content: What is the capital of France? + role: user + - content: The capital of France is Paris. + role: assistant + - content: Tell me about the Eiffel Tower. + role: user + instructions: You are a helpful assistant. + model: gpt-4o + stream: false + tool_choice: auto + tools: + - type: file_search + file_store_ids: + - vs_691f9b2716c08191bd8a0d05c5008de1 + uri: https://api.openai.com/v1/responses + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '2662' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '3916' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + background: false + billing: + payer: developer + created_at: 1763679022 + error: null + id: resp_048b7aacae0da42e00691f9b2e64008194bcd0b9ba860b2739 + incomplete_details: null + instructions: You are a helpful assistant. + max_output_tokens: null + max_tool_calls: null + metadata: {} + model: gpt-4o-2024-08-06 + object: response + output: + - id: fs_048b7aacae0da42e00691f9b2efb188194b397bbe4e63570c8 + queries: + - Eiffel Tower + results: null + status: completed + type: file_search_call + - content: + - annotations: [] + logprobs: [] + text: "I couldn't find any information about the Eiffel Tower in the provided documents. However, here's some general + information:\n\nThe Eiffel Tower is an iconic landmark located in Paris, France. It was designed by the engineer + Gustave Eiffel and completed in 1889 for the World's Fair. Standing at 324 meters (1,063 feet), it was the tallest + man-made structure in the world until the completion of the Chrysler Building in New York in 1930. The tower is + a global cultural icon of France and one of the most recognizable structures in the world. It is also one of the + most-visited paid monuments, attracting millions of tourists every year. \n\nWould you like to know anything specific + about the Eiffel Tower?" + type: output_text + id: msg_048b7aacae0da42e00691f9b2fc864819481f12d02019a247b + role: assistant + status: completed + type: message + parallel_tool_calls: true + previous_response_id: null + prompt_cache_key: null + prompt_cache_retention: null + reasoning: + effort: null + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: auto + tools: + - filters: null + max_num_results: 20 + ranking_options: + ranker: auto + score_threshold: 0.0 + type: file_search + file_store_ids: + - vs_691f9b2716c08191bd8a0d05c5008de1 + top_logprobs: 0 + top_p: 1.0 + truncation: disabled + usage: + input_tokens: 887 + input_tokens_details: + cached_tokens: 0 + output_tokens: 160 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 1047 + user: null + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-type: + - application/json + cookie: + - __cf_bm=E5f.uTRtWuVMWvVfIOlPnmO1VssA9e0wxm.ib6Am5PY-1763679021-1.0.1.1-Jy7.32_vn8tOGfOexRT6Q3s2w5kUE.99qJmB7KDtVCKlqpFuTCEJGwSFII_dqYG7CRuR_VO6mRShith0PiLMFsSsFMZHKSYYoxxSWDVGz3o; + _cfuvid=Qk6c2yE2yjyTQsPJH9hhq6EAYojWx242WTVJbNnFLLI-1763679021903-0.0.1.1-604800000 + host: + - api.openai.com + method: DELETE + uri: https://api.openai.com/v1/files/file-5SYDnrNwrDK7shmFdZLZYJ + response: + headers: + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '81' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '653' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + deleted: true + id: file-5SYDnrNwrDK7shmFdZLZYJ + object: file + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-type: + - application/json + cookie: + - __cf_bm=Noytj27FWeTM2taPxfXsTMm3RZCX9WCVTg8bAHrfPd4-1763679027-1.0.1.1-paKZxZ9hNmxXkvQQ1kSO_t9ni6lkeTd4GWcSNMKnsiQHQeJ5LQJCYuj5Ql3jzk9QMMIMfcQvrkHjqXZgGSYUR6SNI._VvAuwCDFNdYXeFjo; + _cfuvid=HgTw7lpyYeugLiNgSJm_t2tP6c8Z6R2tauCQgk4jtt4-1763679027076-0.0.1.1-604800000 + host: + - api.openai.com + openai-beta: + - assistants=v2 + method: DELETE + uri: https://api.openai.com/v1/vector_stores/vs_691f9b2716c08191bd8a0d05c5008de1 + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '104' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '1707' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + deleted: true + id: vs_691f9b2716c08191bd8a0d05c5008de1 + object: vector_store.deleted + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_openai_responses/test_openai_responses_model_file_search_tool_stream.yaml b/tests/models/cassettes/test_openai_responses/test_openai_responses_model_file_search_tool_stream.yaml new file mode 100644 index 0000000000..531eb48f1b --- /dev/null +++ b/tests/models/cassettes/test_openai_responses/test_openai_responses_model_file_search_tool_stream.yaml @@ -0,0 +1,404 @@ +interactions: +- request: + body: "--2ee2a5cb138c61feb3e2610a3b483d9f\r\nContent-Disposition: form-data; name=\"purpose\"\r\n\r\nassistants\r\n--2ee2a5cb138c61feb3e2610a3b483d9f\r\nContent-Disposition: + form-data; name=\"file\"; filename=\"tmpy8qaz9hx.txt\"\r\nContent-Type: text/plain\r\n\r\nParis is the capital of France. + It is known for the Eiffel Tower.\r\n--2ee2a5cb138c61feb3e2610a3b483d9f--\r\n" + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '340' + content-type: + - multipart/form-data; boundary=2ee2a5cb138c61feb3e2610a3b483d9f + host: + - api.openai.com + method: POST + uri: https://api.openai.com/v1/files + response: + headers: + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '238' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '271' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + bytes: 65 + created_at: 1763679211 + expires_at: null + filename: tmpy8qaz9hx.txt + id: file-CSgtJWZR52QLmC4rnLuN1Q + object: file + purpose: assistants + status: processed + status_details: null + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '34' + content-type: + - application/json + cookie: + - __cf_bm=ZaHjsahqxzJuMmY7t8u4pPQYdlGe.1by7Irs2Hu3TR0-1763679211-1.0.1.1-Rlchtqj9G7Gwl63pN2bdSk8OQaS_l4_UY41l48riuuhL7TBDMXyZUnQgJcAESnpKWabIXaAOA_tg1vIHT51LwFGxXeHtWlE1XkfS8wEqobU; + _cfuvid=dd6ZWZPIRvtxUEKLBRNu.gry2_dpD0L_SwYEnMivXKE-1763679211945-0.0.1.1-604800000 + host: + - api.openai.com + openai-beta: + - assistants=v2 + method: POST + parsed_body: + name: test-file-search-stream + uri: https://api.openai.com/v1/vector_stores + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '425' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '475' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + created_at: 1763679212 + description: null + expires_after: null + expires_at: null + file_counts: + cancelled: 0 + completed: 0 + failed: 0 + in_progress: 0 + total: 0 + id: vs_691f9bec0a288191b73aec4456ff43e8 + last_active_at: 1763679212 + metadata: {} + name: test-file-search-stream + object: vector_store + status: completed + usage_bytes: 0 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '41' + content-type: + - application/json + cookie: + - __cf_bm=ZaHjsahqxzJuMmY7t8u4pPQYdlGe.1by7Irs2Hu3TR0-1763679211-1.0.1.1-Rlchtqj9G7Gwl63pN2bdSk8OQaS_l4_UY41l48riuuhL7TBDMXyZUnQgJcAESnpKWabIXaAOA_tg1vIHT51LwFGxXeHtWlE1XkfS8wEqobU; + _cfuvid=dd6ZWZPIRvtxUEKLBRNu.gry2_dpD0L_SwYEnMivXKE-1763679211945-0.0.1.1-604800000 + host: + - api.openai.com + openai-beta: + - assistants=v2 + method: POST + parsed_body: + file_id: file-CSgtJWZR52QLmC4rnLuN1Q + uri: https://api.openai.com/v1/vector_stores/vs_691f9bec0a288191b73aec4456ff43e8/files + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '395' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '940' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + attributes: {} + chunking_strategy: + static: + chunk_overlap_tokens: 400 + max_chunk_size_tokens: 800 + type: static + created_at: 1763679213 + id: file-CSgtJWZR52QLmC4rnLuN1Q + last_error: null + object: vector_store.file + status: in_progress + usage_bytes: 0 + vector_store_id: vs_691f9bec0a288191b73aec4456ff43e8 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '260' + content-type: + - application/json + cookie: + - __cf_bm=ZaHjsahqxzJuMmY7t8u4pPQYdlGe.1by7Irs2Hu3TR0-1763679211-1.0.1.1-Rlchtqj9G7Gwl63pN2bdSk8OQaS_l4_UY41l48riuuhL7TBDMXyZUnQgJcAESnpKWabIXaAOA_tg1vIHT51LwFGxXeHtWlE1XkfS8wEqobU; + _cfuvid=dd6ZWZPIRvtxUEKLBRNu.gry2_dpD0L_SwYEnMivXKE-1763679211945-0.0.1.1-604800000 + host: + - api.openai.com + method: POST + parsed_body: + input: + - content: What is the capital of France? + role: user + instructions: You are a helpful assistant. + model: gpt-4o + stream: true + tool_choice: auto + tools: + - type: file_search + file_store_ids: + - vs_691f9bec0a288191b73aec4456ff43e8 + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0563c3212c52abee00691f9bf06c0481a19e4a5cc8dbd5b4e2","object":"response","created_at":1763679216,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"file_search","filters":null,"max_num_results":20,"ranking_options":{"ranker":"auto","score_threshold":0.0},"file_store_ids":["vs_691f9bec0a288191b73aec4456ff43e8"]}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + + event: response.in_progress + data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0563c3212c52abee00691f9bf06c0481a19e4a5cc8dbd5b4e2","object":"response","created_at":1763679216,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"file_search","filters":null,"max_num_results":20,"ranking_options":{"ranker":"auto","score_threshold":0.0},"file_store_ids":["vs_691f9bec0a288191b73aec4456ff43e8"]}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + + event: response.output_item.added + data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"fs_0563c3212c52abee00691f9bf17e3481a1b9d846022d1b8c5c","type":"file_search_call","status":"in_progress","queries":[],"results":null}} + + event: response.file_search_call.in_progress + data: {"type":"response.file_search_call.in_progress","sequence_number":3,"output_index":0,"item_id":"fs_0563c3212c52abee00691f9bf17e3481a1b9d846022d1b8c5c"} + + event: response.file_search_call.searching + data: {"type":"response.file_search_call.searching","sequence_number":4,"output_index":0,"item_id":"fs_0563c3212c52abee00691f9bf17e3481a1b9d846022d1b8c5c"} + + event: response.file_search_call.completed + data: {"type":"response.file_search_call.completed","sequence_number":5,"output_index":0,"item_id":"fs_0563c3212c52abee00691f9bf17e3481a1b9d846022d1b8c5c"} + + event: response.output_item.done + data: {"type":"response.output_item.done","sequence_number":6,"output_index":0,"item":{"id":"fs_0563c3212c52abee00691f9bf17e3481a1b9d846022d1b8c5c","type":"file_search_call","status":"completed","queries":["What is the capital of France?"],"results":null}} + + event: response.output_item.added + data: {"type":"response.output_item.added","sequence_number":7,"output_index":1,"item":{"id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","type":"message","status":"in_progress","content":[],"role":"assistant"}} + + event: response.content_part.added + data: {"type":"response.content_part.added","sequence_number":8,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"YPvmalPDSBfSi"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":" capital","logprobs":[],"obfuscation":"1uOhub4V"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"vTpoUBUZqqbws"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":" France","logprobs":[],"obfuscation":"ZW0RzADhP"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"oXIyIIAflfbIR"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":" Paris","logprobs":[],"obfuscation":"D5BQoh2CpI"} + + event: response.output_text.annotation.added + data: {"type":"response.output_text.annotation.added","sequence_number":15,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"annotation_index":0,"annotation":{"type":"file_citation","file_id":"file-CSgtJWZR52QLmC4rnLuN1Q","filename":"tmpy8qaz9hx.txt","index":30}} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"delta":".","logprobs":[],"obfuscation":"KoS1P0uEJrkx0Xg"} + + event: response.output_text.done + data: {"type":"response.output_text.done","sequence_number":17,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"text":"The capital of France is Paris.","logprobs":[]} + + event: response.content_part.done + data: {"type":"response.content_part.done","sequence_number":18,"item_id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[{"type":"file_citation","file_id":"file-CSgtJWZR52QLmC4rnLuN1Q","filename":"tmpy8qaz9hx.txt","index":30}],"logprobs":[],"text":"The capital of France is Paris."}} + + event: response.output_item.done + data: {"type":"response.output_item.done","sequence_number":19,"output_index":1,"item":{"id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","type":"message","status":"completed","content":[{"type":"output_text","annotations":[{"type":"file_citation","file_id":"file-CSgtJWZR52QLmC4rnLuN1Q","filename":"tmpy8qaz9hx.txt","index":30}],"logprobs":[],"text":"The capital of France is Paris."}],"role":"assistant"}} + + event: response.completed + data: {"type":"response.completed","sequence_number":20,"response":{"id":"resp_0563c3212c52abee00691f9bf06c0481a19e4a5cc8dbd5b4e2","object":"response","created_at":1763679216,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-2024-08-06","output":[{"id":"fs_0563c3212c52abee00691f9bf17e3481a1b9d846022d1b8c5c","type":"file_search_call","status":"completed","queries":["What is the capital of France?"],"results":null},{"id":"msg_0563c3212c52abee00691f9bf2915081a1b1302f1804d02747","type":"message","status":"completed","content":[{"type":"output_text","annotations":[{"type":"file_citation","file_id":"file-CSgtJWZR52QLmC4rnLuN1Q","filename":"tmpy8qaz9hx.txt","index":30}],"logprobs":[],"text":"The capital of France is Paris."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"file_search","filters":null,"max_num_results":20,"ranking_options":{"ranker":"auto","score_threshold":0.0},"file_store_ids":["vs_691f9bec0a288191b73aec4456ff43e8"]}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":1172,"input_tokens_details":{"cached_tokens":0},"output_tokens":36,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1208},"user":null,"metadata":{}}} + + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-type: + - text/event-stream; charset=utf-8 + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '80' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-type: + - application/json + cookie: + - __cf_bm=teJAG1Ygwg6pNqghDBXSGP2Fv.xom_voQruBNcHVxRo-1763679216-1.0.1.1-XG3F7XRpl6LQB5iqMMlRiOpDgTWI4X5Lr6fdSo0DiD3th45bsIQOksZJr1ejSqshSWNI.xTsE1VufBivg68e12Ur2bWgiOKDw.nVcQ_5uMI; + _cfuvid=2Yuo1pLsfVamacrbPTP8yU75fL4hSPtwCmri0QjD.LI-1763679216514-0.0.1.1-604800000 + host: + - api.openai.com + method: DELETE + uri: https://api.openai.com/v1/files/file-CSgtJWZR52QLmC4rnLuN1Q + response: + headers: + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '81' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '493' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + deleted: true + id: file-CSgtJWZR52QLmC4rnLuN1Q + object: file + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-type: + - application/json + cookie: + - __cf_bm=1kwntOT5vP.a7x2J.oS72iWrl5agb16.70EsZWXWm7o-1763679219-1.0.1.1-1cnRU_g1H__oIwVG3A1ecLMPx7O55d8M0330R.dk2unvEnyRVRaafijGmMyJ4vBGWqKuAfEIYapyiIDYgt0x6pZIOTNiOdQTwtAixna3ls8; + _cfuvid=GEyfSlsI3Ab4VlNtEr1UKHRTAk_Aq9sB1c98MF_us80-1763679219822-0.0.1.1-604800000 + host: + - api.openai.com + openai-beta: + - assistants=v2 + method: DELETE + uri: https://api.openai.com/v1/vector_stores/vs_691f9bec0a288191b73aec4456ff43e8 + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '104' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '936' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + deleted: true + id: vs_691f9bec0a288191b73aec4456ff43e8 + object: vector_store.deleted + status: + code: 200 + message: OK +version: 1 +... diff --git a/tests/models/test_google.py b/tests/models/test_google.py index 613ec19e4f..8ac9d0ed61 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -1,9 +1,11 @@ from __future__ import annotations as _annotations +import asyncio import base64 import datetime import os import re +import tempfile from collections.abc import AsyncIterator from typing import Any @@ -44,7 +46,13 @@ VideoUrl, ) from pydantic_ai.agent import Agent -from pydantic_ai.builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool +from pydantic_ai.builtin_tools import ( + CodeExecutionTool, + FileSearchTool, + ImageGenerationTool, + UrlContextTool, + WebSearchTool, +) from pydantic_ai.exceptions import ModelAPIError, ModelHTTPError, ModelRetry, UnexpectedModelBehavior, UserError from pydantic_ai.messages import ( BuiltinToolCallEvent, # pyright: ignore[reportDeprecated] @@ -3607,7 +3615,7 @@ async def response_iterator() -> AsyncIterator[GenerateContentResponse]: model_request_parameters=ModelRequestParameters(), _model_name='gemini-test', _response=response_iterator(), - _timestamp=datetime.datetime.now(datetime.timezone.utc), + _timestamp=IsDatetime(), _provider_name='test-provider', _provider_url='', ) @@ -3638,6 +3646,473 @@ def _generate_response_with_texts(response_id: str, texts: list[str]) -> Generat ) +@pytest.mark.vcr() +async def test_google_model_file_search_tool(allow_model_requests: None, google_provider: GoogleProvider): + client = google_provider.client + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write('Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.') + test_file_path = f.name + + store = None + try: + store = await client.aio.file_search_stores.create(config={'display_name': 'test-file-search-store'}) + assert store.name is not None + + with open(test_file_path, 'rb') as f: + await client.aio.file_search_stores.upload_to_file_search_store( + file_search_store_name=store.name, file=f, config={'mime_type': 'text/plain'} + ) + + await asyncio.sleep(3) + + m = GoogleModel('gemini-2.5-pro', provider=google_provider) + agent = Agent( + m, + system_prompt='You are a helpful assistant.', + builtin_tools=[FileSearchTool(file_store_ids=[store.name])], + ) + + result = await agent.run('What is the capital of France?') + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + SystemPromptPart( + content='You are a helpful assistant.', + timestamp=IsDatetime(), + ), + UserPromptPart( + content='What is the capital of France?', + timestamp=IsDatetime(), + ), + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + args={}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + }, + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + }, + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ), + TextPart( + content="""\ +The capital of France is Paris. + +Paris is a major global center for art, fashion, gastronomy, and culture. A famous landmark in the city is the Eiffel Tower. The city is also known for its 19th-century cityscape, which is crisscrossed by wide boulevards and the River Seine.\ +""" + ), + ], + usage=RequestUsage( + input_tokens=15, + output_tokens=2464, + details={ + 'thoughts_tokens': 879, + 'tool_use_prompt_tokens': 1485, + 'text_prompt_tokens': 15, + 'text_tool_use_prompt_tokens': 1485, + }, + ), + model_name='gemini-2.5-pro', + timestamp=IsDatetime(), + provider_name='google-gla', + provider_details={'finish_reason': 'STOP'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + messages = result.all_messages() + result = await agent.run(user_prompt='Tell me about the Eiffel Tower.', message_history=messages) + assert result.new_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='Tell me about the Eiffel Tower.', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + args={}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ), + TextPart( + content="""\ +The Eiffel Tower is a famous landmark located in Paris, the capital of France. It is one of the most recognizable structures in the world. + +Here are some key facts about the Eiffel Tower: + +* **Construction:** It was designed by the engineer Gustave Eiffel, and his company built it between 1887 and 1889. It was created as the entrance arch for the 1889 Exposition Universelle (World's Fair), which celebrated the 100th anniversary of the French Revolution. +* **Design and Material:** The tower is made of wrought iron and is a masterpiece of structural engineering. Its open-lattice design was innovative for its time and was chosen for its wind resistance and stability. +* **Height:** The Eiffel Tower stands at a height of 330 meters (1,083 feet), including its antennas. For about 41 years, it held the title of the tallest man-made structure in the world until the Chrysler Building in New York City was completed in 1930. +* **Visiting:** The tower has three levels for visitors. The first two levels can be reached by stairs or elevators and feature restaurants and shops. The top level, accessible only by elevator, offers panoramic views of Paris. +* **Cultural Impact:** Initially criticized by some of France's leading artists and intellectuals for its design, the Eiffel Tower has since become a global cultural icon of France and a symbol of Paris itself. It is one of the most visited paid monuments in the world.\ +""" + ), + ], + usage=RequestUsage( + input_tokens=88, + output_tokens=776, + details={ + 'thoughts_tokens': 167, + 'tool_use_prompt_tokens': 270, + 'text_prompt_tokens': 88, + 'text_tool_use_prompt_tokens': 270, + }, + ), + model_name='gemini-2.5-pro', + timestamp=IsDatetime(), + provider_name='google-gla', + provider_details={'finish_reason': 'STOP'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + finally: + os.unlink(test_file_path) + if store is not None and store.name is not None: + await client.aio.file_search_stores.delete(name=store.name, config={'force': True}) + + +@pytest.mark.vcr() +async def test_google_model_file_search_tool_stream(allow_model_requests: None, google_provider: GoogleProvider): + client = google_provider.client + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write('Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.') + test_file_path = f.name + + store = None + try: + store = await client.aio.file_search_stores.create(config={'display_name': 'test-file-search-stream'}) + assert store.name is not None + + with open(test_file_path, 'rb') as f: + await client.aio.file_search_stores.upload_to_file_search_store( + file_search_store_name=store.name, file=f, config={'mime_type': 'text/plain'} + ) + + await asyncio.sleep(3) + + m = GoogleModel('gemini-2.5-pro', provider=google_provider) + agent = Agent( + m, + system_prompt='You are a helpful assistant.', + builtin_tools=[FileSearchTool(file_store_ids=[store.name])], + ) + + event_parts: list[Any] = [] + async with agent.iter(user_prompt='What is the capital of France?') as agent_run: + async for node in agent_run: + if Agent.is_model_request_node(node) or Agent.is_call_tools_node(node): + async with node.stream(agent_run.ctx) as request_stream: + async for event in request_stream: + event_parts.append(event) + + assert agent_run.result is not None + messages = agent_run.result.all_messages() + assert messages == snapshot( + [ + ModelRequest( + parts=[ + SystemPromptPart( + content='You are a helpful assistant.', + timestamp=IsDatetime(), + ), + UserPromptPart( + content='What is the capital of France?', + timestamp=IsDatetime(), + ), + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ), + BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + TextPart(content='The capital of France is Paris. A famous landmark in Paris is the Eiffel'), + BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ), + TextPart(content=' Tower.'), + ], + usage=RequestUsage( + input_tokens=15, + output_tokens=790, + details={ + 'thoughts_tokens': 211, + 'tool_use_prompt_tokens': 526, + 'text_prompt_tokens': 15, + 'text_tool_use_prompt_tokens': 526, + }, + ), + model_name='gemini-2.5-pro', + timestamp=IsDatetime(), + provider_name='google-gla', + provider_details={'finish_reason': 'STOP'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + assert event_parts == snapshot( + [ + PartStartEvent( + index=0, + part=BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + ), + PartEndEvent( + index=0, + part=BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + next_part_kind='builtin-tool-return', + ), + PartStartEvent( + index=1, + part=BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ), + previous_part_kind='builtin-tool-call', + ), + PartStartEvent( + index=2, + part=BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + previous_part_kind='builtin-tool-return', + ), + PartEndEvent( + index=2, + part=BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ), + next_part_kind='text', + ), + PartStartEvent( + index=3, part=TextPart(content='The capital of France'), previous_part_kind='builtin-tool-call' + ), + FinalResultEvent(tool_name=None, tool_call_id=None), + PartDeltaEvent( + index=3, delta=TextPartDelta(content_delta=' is Paris. A famous landmark in Paris is the Eiffel') + ), + PartEndEvent( + index=3, + part=TextPart(content='The capital of France is Paris. A famous landmark in Paris is the Eiffel'), + next_part_kind='builtin-tool-return', + ), + PartStartEvent( + index=4, + part=BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ), + previous_part_kind='text', + ), + PartStartEvent(index=5, part=TextPart(content=' Tower.'), previous_part_kind='builtin-tool-return'), + PartEndEvent(index=5, part=TextPart(content=' Tower.')), + BuiltinToolCallEvent( # pyright: ignore[reportDeprecated] + part=BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ) + ), + BuiltinToolResultEvent( # pyright: ignore[reportDeprecated] + result=BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ) + ), + BuiltinToolCallEvent( # pyright: ignore[reportDeprecated] + part=BuiltinToolCallPart( + tool_name='file_search', + args={'query': 'what is the capital of France?'}, + tool_call_id=IsStr(), + provider_name='google-gla', + ) + ), + BuiltinToolResultEvent( # pyright: ignore[reportDeprecated] + result=BuiltinToolReturnPart( + tool_name='file_search', + content={ + 'retrieved_contexts': [ + { + 'document_name': None, + 'rag_chunk': None, + 'text': 'Paris is the capital of France. The Eiffel Tower is a famous landmark in Paris.', + 'title': None, + 'uri': None, + } + ] + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='google-gla', + ) + ), + ] + ) + + finally: + os.unlink(test_file_path) + if store is not None and store.name is not None: + await client.aio.file_search_stores.delete(name=store.name, config={'force': True}) + + async def test_cache_point_filtering(): """Test that CachePoint is filtered out in Google internal method.""" from pydantic_ai import CachePoint @@ -4014,3 +4489,103 @@ def test_google_missing_tool_call_thought_signature(): ], } ) + + +async def test_google_file_search_grounding_chunks_empty(): + from google.genai.types import GroundingMetadata + + chunk = GenerateContentResponse.model_validate( + { + 'candidates': [ + { + 'content': { + 'role': 'model', + 'parts': [{'text': 'test'}], + }, + 'finish_reason': GoogleFinishReason.STOP, + 'grounding_metadata': GroundingMetadata.model_validate( + { + 'grounding_chunks': [], + } + ), + } + ], + } + ) + + async def response_iterator() -> AsyncIterator[GenerateContentResponse]: + yield chunk + + streamed_response = GeminiStreamedResponse( + model_request_parameters=ModelRequestParameters(), + _model_name='gemini-1.5-flash', + _response=response_iterator(), + _timestamp=datetime.datetime.now(datetime.timezone.utc), + _provider_name='google-gla', + _provider_url='https://generativelanguage.googleapis.com', + ) + streamed_response._file_search_tool_call_id = 'test-id' # pyright: ignore[reportPrivateUsage] + + events = [event async for event in streamed_response._get_event_iterator()] # pyright: ignore[reportPrivateUsage] + assert len(events) > 0 + + +async def test_google_file_search_retrieved_contexts_empty(): + from google.genai.types import GroundingChunk, GroundingMetadata + + chunk = GenerateContentResponse.model_validate( + { + 'candidates': [ + { + 'content': { + 'role': 'model', + 'parts': [{'text': 'test'}], + }, + 'finish_reason': GoogleFinishReason.STOP, + 'grounding_metadata': GroundingMetadata.model_validate( + { + 'grounding_chunks': [ + GroundingChunk.model_validate( + { + 'retrieved_context': None, + } + ) + ], + } + ), + } + ], + } + ) + + async def response_iterator() -> AsyncIterator[GenerateContentResponse]: + yield chunk + + streamed_response = GeminiStreamedResponse( + model_request_parameters=ModelRequestParameters(), + _model_name='gemini-1.5-flash', + _response=response_iterator(), + _timestamp=datetime.datetime.now(datetime.timezone.utc), + _provider_name='google-gla', + _provider_url='https://generativelanguage.googleapis.com', + ) + streamed_response._file_search_tool_call_id = 'test-id' # pyright: ignore[reportPrivateUsage] + + events = [event async for event in streamed_response._get_event_iterator()] # pyright: ignore[reportPrivateUsage] + assert len(events) > 0 + + +async def test_google_file_search_cleanup_none(): + client = GoogleProvider(api_key='test-key').client + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write('test content') + test_file_path = f.name + + store = None + try: + pass + finally: + os.unlink(test_file_path) + if store is not None and store.name is not None: + await client.aio.file_search_stores.delete(name=store.name, config={'force': True}) diff --git a/tests/models/test_openai_responses.py b/tests/models/test_openai_responses.py index 7433841cde..da2f042375 100644 --- a/tests/models/test_openai_responses.py +++ b/tests/models/test_openai_responses.py @@ -7442,3 +7442,503 @@ def get_meaning_of_life() -> int: }, ] ) + + +@pytest.mark.vcr() +async def test_openai_responses_model_file_search_tool(allow_model_requests: None, openai_api_key: str): + import asyncio + import os + import tempfile + + from openai import AsyncOpenAI + + from pydantic_ai.builtin_tools import FileSearchTool + from pydantic_ai.providers.openai import OpenAIProvider + + async_client = AsyncOpenAI(api_key=openai_api_key) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write('Paris is the capital of France. It is known for the Eiffel Tower.') + test_file_path = f.name + + file = None + vector_store = None + try: + with open(test_file_path, 'rb') as f: + file = await async_client.files.create(file=f, purpose='assistants') + + vector_store = await async_client.vector_stores.create(name='test-file-search') + await async_client.vector_stores.files.create(vector_store_id=vector_store.id, file_id=file.id) + + await asyncio.sleep(2) + + m = OpenAIResponsesModel('gpt-4o', provider=OpenAIProvider(openai_client=async_client)) + agent = Agent( + m, + instructions='You are a helpful assistant.', + builtin_tools=[FileSearchTool(file_store_ids=[vector_store.id])], + ) + + result = await agent.run('What is the capital of France?') + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is the capital of France?', + timestamp=IsDatetime(), + ) + ], + instructions='You are a helpful assistant.', + run_id=IsStr(), + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + args={'queries': ['What is the capital of France?']}, + tool_call_id=IsStr(), + provider_name='openai', + ), + BuiltinToolReturnPart( + tool_name='file_search', + content={'status': 'completed'}, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='openai', + ), + TextPart( + content='The capital of France is Paris.', + id=IsStr(), + ), + ], + usage=RequestUsage(input_tokens=870, output_tokens=30, details={'reasoning_tokens': 0}), + model_name='gpt-4o-2024-08-06', + timestamp=IsDatetime(), + provider_name='openai', + provider_details={'finish_reason': 'completed'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + messages = result.all_messages() + result = await agent.run(user_prompt='Tell me about the Eiffel Tower.', message_history=messages) + assert result.new_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='Tell me about the Eiffel Tower.', + timestamp=IsDatetime(), + ) + ], + instructions='You are a helpful assistant.', + run_id=IsStr(), + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + args={'queries': ['Eiffel Tower']}, + tool_call_id=IsStr(), + provider_name='openai', + ), + BuiltinToolReturnPart( + tool_name='file_search', + content={'status': 'completed'}, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='openai', + ), + TextPart( + content="""\ +I couldn't find any information about the Eiffel Tower in the provided documents. However, here's some general information: + +The Eiffel Tower is an iconic landmark located in Paris, France. It was designed by the engineer Gustave Eiffel and completed in 1889 for the World's Fair. Standing at 324 meters (1,063 feet), it was the tallest man-made structure in the world until the completion of the Chrysler Building in New York in 1930. The tower is a global cultural icon of France and one of the most recognizable structures in the world. It is also one of the most-visited paid monuments, attracting millions of tourists every year. \n\ + +Would you like to know anything specific about the Eiffel Tower?\ +""", + id=IsStr(), + ), + ], + usage=RequestUsage(input_tokens=887, output_tokens=160, details={'reasoning_tokens': 0}), + model_name='gpt-4o-2024-08-06', + timestamp=IsDatetime(), + provider_name='openai', + provider_details={'finish_reason': 'completed'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + finally: + os.unlink(test_file_path) + if file is not None: + await async_client.files.delete(file.id) + if vector_store is not None: + await async_client.vector_stores.delete(vector_store.id) + await async_client.close() + + +@pytest.mark.vcr() +async def test_openai_responses_model_file_search_tool_stream(allow_model_requests: None, openai_api_key: str): + import asyncio + import os + import tempfile + from typing import Any + + from openai import AsyncOpenAI + + from pydantic_ai.builtin_tools import FileSearchTool + from pydantic_ai.providers.openai import OpenAIProvider + + async_client = AsyncOpenAI(api_key=openai_api_key) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write('Paris is the capital of France. It is known for the Eiffel Tower.') + test_file_path = f.name + + file = None + vector_store = None + try: + with open(test_file_path, 'rb') as f: + file = await async_client.files.create(file=f, purpose='assistants') + + vector_store = await async_client.vector_stores.create(name='test-file-search-stream') + await async_client.vector_stores.files.create(vector_store_id=vector_store.id, file_id=file.id) + + await asyncio.sleep(2) + + m = OpenAIResponsesModel('gpt-4o', provider=OpenAIProvider(openai_client=async_client)) + agent = Agent( + m, + instructions='You are a helpful assistant.', + builtin_tools=[FileSearchTool(file_store_ids=[vector_store.id])], + ) + + event_parts: list[Any] = [] + async with agent.iter(user_prompt='What is the capital of France?') as agent_run: + async for node in agent_run: + if Agent.is_model_request_node(node) or Agent.is_call_tools_node(node): + async with node.stream(agent_run.ctx) as request_stream: + async for event in request_stream: + event_parts.append(event) + + assert agent_run.result is not None + messages = agent_run.result.all_messages() + assert messages == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is the capital of France?', + timestamp=IsDatetime(), + ) + ], + instructions='You are a helpful assistant.', + run_id=IsStr(), + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + args={'queries': ['What is the capital of France?']}, + tool_call_id=IsStr(), + provider_name='openai', + ), + BuiltinToolReturnPart( + tool_name='file_search', + content={'status': 'completed'}, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='openai', + ), + TextPart( + content='The capital of France is Paris.', + id=IsStr(), + ), + ], + usage=RequestUsage(input_tokens=1172, output_tokens=36, details={'reasoning_tokens': 0}), + model_name='gpt-4o-2024-08-06', + timestamp=IsDatetime(), + provider_name='openai', + provider_details={'finish_reason': 'completed'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + assert event_parts == snapshot( + [ + PartStartEvent( + index=0, + part=BuiltinToolCallPart( + tool_name='file_search', + tool_call_id=IsStr(), + provider_name='openai', + ), + ), + PartDeltaEvent( + index=0, + delta=ToolCallPartDelta( + args_delta={'queries': ['What is the capital of France?']}, + tool_call_id=IsStr(), + ), + ), + PartEndEvent( + index=0, + part=BuiltinToolCallPart( + tool_name='file_search', + args={'queries': ['What is the capital of France?']}, + tool_call_id=IsStr(), + provider_name='openai', + ), + next_part_kind='builtin-tool-return', + ), + PartStartEvent( + index=1, + part=BuiltinToolReturnPart( + tool_name='file_search', + content={'status': 'completed'}, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='openai', + ), + previous_part_kind='builtin-tool-call', + ), + PartStartEvent( + index=2, + part=TextPart(content='The', id=IsStr()), + previous_part_kind='builtin-tool-return', + ), + FinalResultEvent(tool_name=None, tool_call_id=None), + PartDeltaEvent(index=2, delta=TextPartDelta(content_delta=' capital')), + PartDeltaEvent(index=2, delta=TextPartDelta(content_delta=' of')), + PartDeltaEvent(index=2, delta=TextPartDelta(content_delta=' France')), + PartDeltaEvent(index=2, delta=TextPartDelta(content_delta=' is')), + PartDeltaEvent(index=2, delta=TextPartDelta(content_delta=' Paris')), + PartDeltaEvent(index=2, delta=TextPartDelta(content_delta='.')), + PartEndEvent( + index=2, + part=TextPart( + content='The capital of France is Paris.', + id=IsStr(), + ), + ), + BuiltinToolCallEvent( # pyright: ignore[reportDeprecated] + part=BuiltinToolCallPart( + tool_name='file_search', + args={'queries': ['What is the capital of France?']}, + tool_call_id=IsStr(), + provider_name='openai', + ) + ), + BuiltinToolResultEvent( # pyright: ignore[reportDeprecated] + result=BuiltinToolReturnPart( + tool_name='file_search', + content={'status': 'completed'}, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='openai', + ) + ), + ] + ) + + finally: + os.unlink(test_file_path) + if file is not None: + await async_client.files.delete(file.id) + if vector_store is not None: + await async_client.vector_stores.delete(vector_store.id) + await async_client.close() + + +async def test_openai_file_search_include_results_setting(): + from openai.types.responses.response_file_search_tool_call import ResponseFileSearchToolCall + + from pydantic_ai.models.openai import _map_file_search_tool_call # pyright: ignore[reportPrivateUsage] + + item = ResponseFileSearchToolCall.model_validate( + { + 'id': 'test-id', + 'queries': ['test query'], + 'status': 'completed', + 'results': None, + 'type': 'file_search_call', + } + ) + + call_part, return_part = _map_file_search_tool_call(item, 'openai') + assert call_part.tool_name == 'file_search' + assert return_part.content == {'status': 'completed'} + + +async def test_openai_include_file_search_results_setting(): + from pydantic_ai.builtin_tools import FileSearchTool + + mock_response = MockOpenAIResponses.create_mock( + response_message( + [ + ResponseOutputMessage( + id='output-1', + content=cast( + list[Content], + [ResponseOutputText(text='Done.', type='output_text', annotations=[])], + ), + role='assistant', + status='completed', + type='message', + ) + ] + ) + ) + + model = OpenAIResponsesModel( + 'gpt-4o', + provider=OpenAIProvider(openai_client=mock_response), + settings=OpenAIResponsesModelSettings(openai_include_file_search_results=True), + ) + agent = Agent( + model=model, + builtin_tools=[FileSearchTool(file_store_ids=[])], + ) + + await agent.run('test') + kwargs = get_mock_responses_kwargs(mock_response) + assert 'include' in kwargs[0] + assert 'file_search_call.results' in kwargs[0]['include'] + + +async def test_openai_file_search_with_message_history(): + from pydantic_ai.builtin_tools import FileSearchTool + + mock_response = MockOpenAIResponses.create_mock( + response_message( + [ + ResponseOutputMessage( + id='output-1', + content=cast( + list[Content], + [ResponseOutputText(text='The capital is Paris.', type='output_text', annotations=[])], + ), + role='assistant', + status='completed', + type='message', + ) + ] + ) + ) + + model = OpenAIResponsesModel('gpt-4o', provider=OpenAIProvider(openai_client=mock_response)) + agent = Agent( + model=model, + builtin_tools=[FileSearchTool(file_store_ids=[])], + ) + + messages = [ + ModelRequest( + parts=[UserPromptPart(content='test')], + run_id='run-1', + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + tool_call_id='file-search-1', + args={'queries': ['test']}, + provider_name='openai', + ) + ], + run_id='run-1', + ), + ] + + result = await agent.run('follow up', message_history=messages) + assert result.output is not None + + +async def test_openai_file_search_status_update(): + from pydantic_ai.builtin_tools import FileSearchTool + + mock_response = MockOpenAIResponses.create_mock( + response_message( + [ + ResponseOutputMessage( + id='output-1', + content=cast( + list[Content], + [ResponseOutputText(text='Done.', type='output_text', annotations=[])], + ), + role='assistant', + status='completed', + type='message', + ) + ] + ) + ) + + model = OpenAIResponsesModel('gpt-4o', provider=OpenAIProvider(openai_client=mock_response)) + agent = Agent( + model=model, + builtin_tools=[FileSearchTool(file_store_ids=[])], + ) + + messages = [ + ModelRequest( + parts=[UserPromptPart(content='test')], + run_id='run-1', + ), + ModelResponse( + parts=[ + BuiltinToolCallPart( + tool_name='file_search', + tool_call_id='file-search-1', + args={'queries': ['test']}, + provider_name='openai', + ), + BuiltinToolReturnPart( + tool_name='file_search', + tool_call_id='file-search-1', + content={'status': 'in_progress'}, + provider_name='openai', + ), + ], + run_id='run-1', + ), + ] + + result = await agent.run('follow up', message_history=messages) + assert result.output is not None + + +async def test_openai_file_search_cleanup_none(): + import os + import tempfile + + from openai import AsyncOpenAI + + async_client = AsyncOpenAI(api_key='test-key') + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write('test content') + test_file_path = f.name + + file = None + vector_store = None + try: + pass + finally: + os.unlink(test_file_path) + if file is not None: + await async_client.files.delete(file.id) + if vector_store is not None: + await async_client.vector_stores.delete(vector_store.id) + await async_client.close() diff --git a/tests/test_builtin_tools.py b/tests/test_builtin_tools.py index 2f636b3e4e..5d417ff440 100644 --- a/tests/test_builtin_tools.py +++ b/tests/test_builtin_tools.py @@ -3,7 +3,7 @@ import pytest from pydantic_ai.agent import Agent -from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool +from pydantic_ai.builtin_tools import CodeExecutionTool, FileSearchTool, WebSearchTool from pydantic_ai.exceptions import UserError from pydantic_ai.models import Model @@ -40,3 +40,22 @@ async def test_builtin_tools_not_supported_code_execution_stream(model: Model, a with pytest.raises(UserError): async with agent.run_stream('What day is tomorrow?'): ... # pragma: no cover + + +@pytest.mark.parametrize( + 'model', ('bedrock', 'mistral', 'cohere', 'huggingface', 'groq', 'anthropic', 'test', 'outlines'), indirect=True +) +async def test_builtin_tools_not_supported_file_search(model: Model, allow_model_requests: None): + agent = Agent(model=model, builtin_tools=[FileSearchTool(file_store_ids=['test-id'])]) + + with pytest.raises(UserError): + await agent.run('Search my files') + + +@pytest.mark.parametrize('model', ('bedrock', 'mistral', 'huggingface', 'groq', 'anthropic', 'outlines'), indirect=True) +async def test_builtin_tools_not_supported_file_search_stream(model: Model, allow_model_requests: None): + agent = Agent(model=model, builtin_tools=[FileSearchTool(file_store_ids=['test-id'])]) + + with pytest.raises(UserError): + async with agent.run_stream('Search my files'): + ... # pragma: no cover diff --git a/tests/test_examples.py b/tests/test_examples.py index 407816b60a..fa4ba592a1 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -115,7 +115,7 @@ def tmp_path_cwd(tmp_path: Path): 'ignore:`BuiltinToolCallEvent` is deprecated', 'ignore:`BuiltinToolResultEvent` is deprecated' ) @pytest.mark.parametrize('example', find_filter_examples()) -def test_docs_examples( +def test_docs_examples( # noqa: C901 example: CodeExample, eval_example: EvalExample, mocker: MockerFixture, @@ -215,6 +215,10 @@ def print(self, *args: Any, **kwargs: Any) -> None: # and https://github.com/pydantic/pytest-examples/issues/46 if 'import DatabaseConn' in example.source: ruff_ignore.append('I001') + # `from pydantic_ai import` and `from pydantic_ai.models.* import` wrongly sorted in imports + # Same pytest-examples issue as DatabaseConn above + if 'from pydantic_ai import' in example.source and 'from pydantic_ai.models.' in example.source: + ruff_ignore.append('I001') if noqa: ruff_ignore.extend(noqa.upper().split())