@@ -171,3 +171,82 @@ def test_vector_stores_search(
171171 LOGGER .info (f"Search mode { search_mode !r} : { len (queries )} queries returned results" )
172172
173173 LOGGER .info (f"Successfully tested vector store search across modes: { search_modes } " )
174+
175+ @pytest .mark .smoke
176+ def test_response_file_search_tool_invocation (
177+ self ,
178+ unprivileged_llama_stack_client : LlamaStackClient ,
179+ llama_stack_models : ModelInfo ,
180+ vector_store_with_example_docs : VectorStore ,
181+ ) -> None :
182+ """
183+ Test that the file_search tool is invoked and returns results via the responses API.
184+
185+ Given: A vector store with pre-uploaded documentation files
186+ When: A question requiring document knowledge is asked with the file_search tool
187+ Then: The response contains a file_search_call output with status 'completed',
188+ results with file_id and filename, and message annotations with file citations
189+ """
190+ response = unprivileged_llama_stack_client .responses .create (
191+ input = IBM_EARNINGS_SEARCH_QUERIES_BY_MODE ["vector" ][0 ],
192+ model = llama_stack_models .model_id ,
193+ instructions = "Always use the file_search tool to look up information before answering." ,
194+ stream = False ,
195+ tools = [
196+ {
197+ "type" : "file_search" ,
198+ "vector_store_ids" : [vector_store_with_example_docs .id ],
199+ }
200+ ],
201+ )
202+
203+ # Verify file_search_call output exists (model invoked the file_search tool)
204+ file_search_calls = [item for item in response .output if item .type == "file_search_call" ]
205+ assert file_search_calls , (
206+ "Expected a file_search_call output item in the response, indicating the model "
207+ f"invoked the file_search tool. Output types: { [item .type for item in response .output ]} "
208+ )
209+
210+ file_search_call = file_search_calls [0 ]
211+ assert file_search_call .status == "completed" , (
212+ f"Expected file_search_call status 'completed', got '{ file_search_call .status } '"
213+ )
214+
215+ # Verify file_search returned results with file metadata
216+ assert file_search_call .results , "file_search_call should contain search results"
217+
218+ for result in file_search_call .results :
219+ assert result .file_id , "Search result must include a non-empty file_id"
220+ assert result .filename , "Search result must include a non-empty filename"
221+ assert result .text , "Search result must include non-empty text content"
222+
223+ LOGGER .info (
224+ f"file_search_call returned { len (file_search_call .results )} result(s). "
225+ f"Filenames: { [r .filename for r in file_search_call .results ]} "
226+ )
227+
228+ # Verify the message contains file_citation annotations
229+ annotations = []
230+ for item in response .output :
231+ if item .type != "message" or not isinstance (item .content , list ):
232+ continue
233+ for content_item in item .content :
234+ if content_item .annotations :
235+ annotations .extend (content_item .annotations )
236+
237+ assert annotations , "Response message should contain file_citation annotations when file_search returns results"
238+
239+ for annotation in annotations :
240+ assert annotation .type == "file_citation" , (
241+ f"Expected annotation type 'file_citation', got '{ annotation .type } '"
242+ )
243+ assert annotation .file_id , "Annotation must include a non-empty file_id"
244+ assert annotation .filename , "Annotation must include a non-empty filename"
245+ assert annotation .index is not None , "Annotation must include an index"
246+
247+ LOGGER .info (
248+ f"Found { len (annotations )} file_citation annotation(s). "
249+ f"File IDs: { [a .file_id for a in annotations ]} . "
250+ f"Filenames: { [a .filename for a in annotations ]} . "
251+ f"Indexes: { [a .index for a in annotations ]} . "
252+ )
0 commit comments