test_file_handling.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. import asyncio
  2. import base64
  3. import logging
  4. import os
  5. import re
  6. import shutil
  7. import time
  8. from pathlib import Path
  9. import pytest
  10. from agents import ModelSettings, ToolCallItem
  11. from openai import AsyncOpenAI, NotFoundError
  12. from agency_swarm import Agency, Agent
  13. OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
  14. @pytest.fixture(scope="module")
  15. async def real_openai_client():
  16. return AsyncOpenAI(api_key=OPENAI_API_KEY)
  17. @pytest.mark.asyncio
  18. async def test_agent_processes_message_files_attachment(real_openai_client: AsyncOpenAI, tmp_path: Path):
  19. """
  20. Tests that an agent can receive a file ID via `file_ids` parameter,
  21. and OpenAI automatically makes the file content available to the LLM.
  22. No custom tools are needed - OpenAI handles file processing automatically.
  23. Uses a REAL OpenAI client for file upload and REAL agent execution.
  24. """
  25. # Use existing rich test PDF from v0.X tests
  26. test_pdf_path = Path("tests/data/files/test-pdf-2.pdf")
  27. assert test_pdf_path.exists(), f"Test PDF not found at {test_pdf_path}"
  28. uploaded_real_file = await real_openai_client.files.create(file=test_pdf_path.open("rb"), purpose="assistants")
  29. attached_file_id = uploaded_real_file.id
  30. print(f"TEST: Uploaded file {test_pdf_path.name} for attachment, ID: {attached_file_id}")
  31. # 2. Initialize Agent WITHOUT any custom file processing tools
  32. # OpenAI will automatically process the attached file and make content available to the LLM
  33. attachment_tester_agent = Agent(
  34. name="AttachmentTesterAgentReal",
  35. instructions=(
  36. "You are a helpful assistant. When files are attached, you can read their content directly. "
  37. "Answer questions about the file content accurately."
  38. ),
  39. model_settings=ModelSettings(temperature=0.0),
  40. )
  41. attachment_tester_agent._openai_client = real_openai_client
  42. # 3. Setup a real Agency for proper testing
  43. agency = Agency(attachment_tester_agent, user_context=None)
  44. # 4. Call get_response with file_ids - OpenAI will automatically process the file
  45. message_to_agent = "What is my favorite food?"
  46. print(f"TEST: Calling get_response for agent '{attachment_tester_agent.name}' with file_ids: [{attached_file_id}]")
  47. response_result = await agency.get_response(message_to_agent, file_ids=[attached_file_id])
  48. assert response_result is not None
  49. assert response_result.final_output is not None
  50. print(f"TEST: Agent final output: {response_result.final_output}")
  51. # 5. Verify the agent could access the file content automatically
  52. # The LLM should be able to read the PDF content without any custom tools
  53. # The test PDF contains a secret phrase we can verify
  54. response_lower = response_result.final_output.lower()
  55. assert len(response_result.final_output) > 20, (
  56. f"Response too short, suggests file content was not processed. Response: {response_result.final_output}"
  57. )
  58. # Look for the secret phrase that should be in the PDF
  59. secret_phrase_found = "strawberry" in response_lower.lower()
  60. assert secret_phrase_found, (
  61. f"Expected word 'strawberry' not found in response. "
  62. f"This suggests the PDF content was not made available to the LLM. "
  63. f"Response: {response_result.final_output}"
  64. )
  65. # 6. Verify NO custom tool calls were made (since OpenAI handles file processing automatically)
  66. tool_calls_found = False
  67. for item in response_result.new_items:
  68. if isinstance(item, ToolCallItem):
  69. tool_calls_found = True
  70. print(f"Unexpected tool call found: {item.raw_item}")
  71. # We expect NO tool calls since OpenAI processes files automatically
  72. assert not tool_calls_found, (
  73. "No tool calls should be found since OpenAI automatically processes file attachments. "
  74. "The presence of tool calls suggests the implementation is incorrectly trying to use custom tools."
  75. )
  76. # 7. Cleanup the test file
  77. try:
  78. await real_openai_client.files.delete(attached_file_id)
  79. print(f"Cleaned up file {attached_file_id}")
  80. except Exception as e:
  81. print(f"Warning: Failed to clean up file {attached_file_id}: {e}")
  82. @pytest.mark.asyncio
  83. async def test_multi_file_type_processing(real_openai_client: AsyncOpenAI, tmp_path: Path):
  84. """
  85. Tests that an agent can process PDF files automatically via OpenAI's Responses API file processing.
  86. NOTE: The OpenAI Responses API with input_file type only supports PDF files for direct attachment.
  87. Other file types (TXT, CSV, images) are supported through different mechanisms:
  88. - Vector Stores/File Search (for RAG functionality)
  89. - Code Interpreter (for code execution with files)
  90. This test focuses on the direct file attachment capability which is PDF-only.
  91. Uses the existing rich test PDF from the v0.X test suite.
  92. """
  93. # Use the existing rich test PDF with secret phrase
  94. test_pdf_path = Path("tests/data/files/test-pdf-2.pdf")
  95. assert test_pdf_path.exists(), f"Test PDF not found at {test_pdf_path}"
  96. # Upload PDF file to OpenAI
  97. with open(test_pdf_path, "rb") as f:
  98. uploaded_file = await real_openai_client.files.create(file=f, purpose="assistants")
  99. file_id = uploaded_file.id
  100. print(f"Uploaded {test_pdf_path.name}, got ID: {file_id}")
  101. try:
  102. # Create an agent WITHOUT custom file processing tools
  103. # OpenAI will automatically process PDF files and make content available
  104. file_processor_agent = Agent(
  105. name="FileProcessorAgent",
  106. instructions="""You are an agent that can read and analyze PDF files automatically.
  107. When PDF files are attached, you can access their content directly.
  108. Extract and summarize key information from the PDF content accurately.""",
  109. model_settings=ModelSettings(temperature=0.0),
  110. )
  111. file_processor_agent._openai_client = real_openai_client
  112. # Initialize agency for the agent
  113. Agency(file_processor_agent, user_context=None)
  114. # Test processing the PDF file
  115. question = "What is my favorite food?"
  116. expected_content = "strawberry"
  117. # Process the PDF file - OpenAI will automatically make file content available
  118. response_result = await file_processor_agent.get_response(question, file_ids=[file_id])
  119. # Verify response
  120. assert response_result is not None
  121. print(f"Response for {test_pdf_path.name}: {response_result.final_output}")
  122. # Use case-insensitive search for matching
  123. response_lower = response_result.final_output.lower()
  124. expected_lower = expected_content.lower()
  125. # With temperature=0, responses should be deterministic
  126. content_found = expected_lower in response_lower
  127. assert content_found, (
  128. f"Expected content '{expected_content}' not found in response for {test_pdf_path.name}. "
  129. f"This suggests OpenAI did not make the PDF file content available to the LLM. "
  130. f"Response: {response_result.final_output}"
  131. )
  132. # Verify NO custom tool calls were made (OpenAI processes PDFs automatically)
  133. tool_calls_found = False
  134. for item in response_result.new_items:
  135. if isinstance(item, ToolCallItem):
  136. tool_calls_found = True
  137. print(f"Unexpected tool call found for {test_pdf_path.name}: {item.raw_item}")
  138. assert not tool_calls_found, (
  139. f"No tool calls should be found for {test_pdf_path.name} since OpenAI automatically processes "
  140. f"PDF file attachments. The presence of tool calls suggests the implementation is incorrectly "
  141. f"trying to use custom tools."
  142. )
  143. finally:
  144. # Cleanup: Delete uploaded file from OpenAI
  145. try:
  146. await real_openai_client.files.delete(file_id=file_id)
  147. print(f"Cleaned up file {file_id}")
  148. except Exception as e:
  149. print(f"Error cleaning up file {file_id}: {e}")
  150. async def _setup_file_search_agent(real_openai_client: AsyncOpenAI, tmp_path: Path):
  151. """Helper to set up file search agent with test file."""
  152. test_txt_path = Path("tests/data/files/favorite_books.txt")
  153. assert test_txt_path.exists(), f"Test file not found at {test_txt_path}"
  154. # Use pytest tmp_path for isolation
  155. tmp_dir = tmp_path / "file_search_test"
  156. tmp_dir.mkdir(exist_ok=True)
  157. tmp_file_path = tmp_dir / "favorite_books.txt"
  158. shutil.copy(test_txt_path, tmp_file_path)
  159. file_search_agent = Agent(
  160. name="FileSearchAgent",
  161. instructions="""You are an agent that can read and analyze text files using FileSearch.
  162. When asked questions about files, always use your FileSearch tool to search through the uploaded documents.
  163. Be direct and specific in your answers based on what you find in the files.""",
  164. model="gpt-5.4-mini",
  165. model_settings=ModelSettings(tool_choice="required"),
  166. include_search_results=True,
  167. tool_use_behavior="stop_on_first_tool",
  168. files_folder=tmp_dir,
  169. )
  170. file_search_agent._openai_client = real_openai_client
  171. # Find vector store folder
  172. candidates = list(tmp_dir.parent.glob(f"{tmp_dir.name}_vs_*"))
  173. folder_path = candidates[0] if candidates else None
  174. assert folder_path, "No vector store folder found"
  175. return file_search_agent, folder_path, tmp_file_path, test_txt_path
  176. async def _wait_for_vector_store(real_openai_client: AsyncOpenAI, agent):
  177. """Helper to wait for vector store processing to complete."""
  178. vector_store_id = agent._associated_vector_store_id
  179. if not vector_store_id:
  180. return
  181. print(f"Waiting for vector store {vector_store_id} to complete processing...")
  182. for i in range(30): # Wait up to 30 seconds
  183. vs = await real_openai_client.vector_stores.retrieve(vector_store_id)
  184. if vs.status == "completed":
  185. print(f"Vector store processing completed after {i + 1} seconds")
  186. break
  187. elif vs.status == "failed":
  188. raise Exception(f"Vector store processing failed: {vs}")
  189. await asyncio.sleep(1)
  190. else:
  191. print(f"Warning: Vector store still processing after 30 seconds, status: {vs.status}")
  192. async def _cleanup_file_search_resources(real_openai_client: AsyncOpenAI, folder_path: Path, agent):
  193. """Helper to clean up file search test resources."""
  194. try:
  195. if folder_path and folder_path.exists():
  196. for file in folder_path.glob("*"):
  197. try:
  198. file_id = agent.file_manager.get_id_from_file(file)
  199. if file_id:
  200. await real_openai_client.files.delete(file_id=file_id)
  201. print(f"Cleaned up file {file.name}")
  202. os.remove(file)
  203. except Exception as e:
  204. print(f"Error cleaning up file {file.name}: {e}")
  205. # Clean up vector store
  206. try:
  207. vector_store_id = folder_path.name.split("_vs_")[-1]
  208. await real_openai_client.vector_stores.delete(vector_store_id=f"vs_{vector_store_id}")
  209. print(f"Cleaned up vector store {folder_path.name}")
  210. os.rmdir(folder_path)
  211. except Exception as e:
  212. print(f"Error cleaning up vector store: {e}")
  213. except Exception as e:
  214. print(f"Error during cleanup: {e}")
  215. async def _assert_openai_file_absent(real_openai_client: AsyncOpenAI, file_id: str, timeout_seconds: int = 120) -> None:
  216. """Polls until the given OpenAI file_id is confirmed deleted, mirroring FileSync waits."""
  217. deadline = asyncio.get_event_loop().time() + timeout_seconds
  218. while True:
  219. try:
  220. await real_openai_client.files.retrieve(file_id=file_id)
  221. except NotFoundError:
  222. return
  223. if asyncio.get_event_loop().time() >= deadline:
  224. pytest.fail(f"OpenAI file {file_id} still exists after waiting {timeout_seconds} seconds")
  225. await asyncio.sleep(1)
  226. @pytest.mark.asyncio
  227. async def test_files_folder_reuse_without_missing_directory_warning(
  228. real_openai_client: AsyncOpenAI, tmp_path: Path, caplog
  229. ):
  230. """Agents reuse existing vector store directories without logging directory warnings."""
  231. caplog.set_level(logging.WARNING, logger="agency_swarm.agent.file_manager")
  232. source_file = Path("tests/data/files/favorite_books.txt")
  233. assert source_file.exists(), f"Test file not found at {source_file}"
  234. files_dir = tmp_path / "files"
  235. files_dir.mkdir()
  236. shutil.copy2(source_file, files_dir / source_file.name)
  237. agent_kwargs = {
  238. "name": "FileReuseAgent",
  239. "instructions": "You are a document assistant who relies on FileSearch for answers.",
  240. "files_folder": str(files_dir),
  241. "include_search_results": True,
  242. "model": "gpt-5.4-mini",
  243. "model_settings": ModelSettings(tool_choice="file_search"),
  244. "tool_use_behavior": "stop_on_first_tool",
  245. }
  246. first_agent = Agent(**agent_kwargs)
  247. first_agent._openai_client = real_openai_client
  248. Agency(first_agent, user_context=None)
  249. await _wait_for_vector_store(real_openai_client, first_agent)
  250. initial_folder = first_agent.files_folder_path
  251. assert initial_folder is not None
  252. assert initial_folder.name.startswith("files_vs_"), f"Unexpected folder name: {initial_folder}"
  253. first_run_logs = "\n".join(
  254. record.message for record in caplog.records if record.name == "agency_swarm.agent.file_manager"
  255. )
  256. assert "not a directory" not in first_run_logs
  257. caplog.clear()
  258. reuse_agent = Agent(**agent_kwargs)
  259. reuse_agent._openai_client = real_openai_client
  260. Agency(reuse_agent, user_context=None)
  261. await _wait_for_vector_store(real_openai_client, reuse_agent)
  262. assert reuse_agent.files_folder_path == initial_folder
  263. reuse_logs = "\n".join(
  264. record.message for record in caplog.records if record.name == "agency_swarm.agent.file_manager"
  265. )
  266. assert "not a directory" not in reuse_logs
  267. try:
  268. await _cleanup_file_search_resources(real_openai_client, initial_folder, reuse_agent)
  269. finally:
  270. # Ensure the original `files` path no longer exists, confirming reuse
  271. assert not files_dir.exists()
  272. @pytest.mark.asyncio
  273. async def test_file_search_tool(real_openai_client: AsyncOpenAI, tmp_path: Path):
  274. """Tests that an agent can use FileSearch tool to process files."""
  275. file_search_agent, folder_path, tmp_file_path, test_txt_path = await _setup_file_search_agent(
  276. real_openai_client, tmp_path
  277. )
  278. try:
  279. await _wait_for_vector_store(real_openai_client, file_search_agent)
  280. # Initialize agency and run test
  281. agency = Agency(file_search_agent, user_context=None)
  282. question = (
  283. "Use FileSearch with the query 'hobbit' to find the answer: What is the title of the 4th book in the list?"
  284. )
  285. try:
  286. from agents import RunConfig
  287. # Single-turn: enforce FileSearch tool usage deterministically
  288. response_result = await agency.get_response(
  289. question,
  290. run_config=RunConfig(model_settings=ModelSettings(tool_choice="file_search")),
  291. )
  292. assert response_result is not None
  293. print(f"Response for {test_txt_path.name}: {response_result.final_output}")
  294. # Verify FileSearch was used and expected content found
  295. final_output_lower = response_result.final_output.lower()
  296. hobbit_found = any(term in final_output_lower for term in ["hobbit", "the hobbit", "j.r.r. tolkien"])
  297. if not hobbit_found:
  298. print("Expected content not found, checking if FileSearch was used")
  299. tool_calls_made = [
  300. item for item in response_result.new_items if hasattr(item, "tool_calls") and item.tool_calls
  301. ]
  302. file_search_used = any(
  303. any(call.type == "file_search" for call in item.tool_calls if hasattr(call, "type"))
  304. for item in tool_calls_made
  305. )
  306. if not file_search_used:
  307. print("FileSearch tool was not used, this may explain why the answer wasn't found")
  308. assert hobbit_found, f"Expected 'hobbit' or related terms not found in: {response_result.final_output}"
  309. except Exception as e:
  310. # Handle 404 errors with retry
  311. if "404" in str(e) and "Files" in str(e):
  312. print(f"Files not found error, re-uploading and retrying: {e}")
  313. uploaded_file_id = file_search_agent.upload_file(str(tmp_file_path), include_in_vector_store=True)
  314. print(f"Re-uploaded file {tmp_file_path.name} with ID: {uploaded_file_id}")
  315. response_result = await agency.get_response(question)
  316. assert response_result is not None
  317. print(f"Response (retry): {response_result.final_output}")
  318. final_output_lower = response_result.final_output.lower()
  319. hobbit_found = any(term in final_output_lower for term in ["hobbit", "the hobbit", "j.r.r. tolkien"])
  320. assert hobbit_found, f"Expected 'hobbit' terms not found in retry: {response_result.final_output}"
  321. else:
  322. raise
  323. finally:
  324. await _cleanup_file_search_resources(real_openai_client, folder_path, file_search_agent)
  325. @pytest.mark.asyncio
  326. async def test_code_interpreter_tool(real_openai_client: AsyncOpenAI, tmp_path: Path):
  327. """
  328. Tests that an agent can read and execute code using CodeInterpreter tool.
  329. """
  330. test_py_path = Path("tests/data/files/test-python.py")
  331. assert test_py_path.exists(), f"Test file not found at {test_py_path}"
  332. # Use pytest tmp_path for isolation
  333. tmp_dir = tmp_path / "code_interpreter_test"
  334. tmp_dir.mkdir(exist_ok=True)
  335. tmp_file_path = tmp_dir / "test-python.py"
  336. shutil.copy(test_py_path, tmp_file_path)
  337. try:
  338. code_interpreter_agent = Agent(
  339. name="CodeInterpreterAgent",
  340. instructions="""You are an agent that can read and execute code using CodeInterpreter tool.""",
  341. model_settings=ModelSettings(temperature=0.0, tool_choice="required"),
  342. tool_use_behavior="stop_on_first_tool",
  343. files_folder=tmp_dir,
  344. )
  345. code_interpreter_agent._openai_client = real_openai_client
  346. # Find vector store folder
  347. candidates = list(tmp_dir.parent.glob(f"{tmp_dir.name}_vs_*"))
  348. folder_path = candidates[0] if candidates else None
  349. assert folder_path, "No vector store folder found"
  350. # Initialize agency for the agent
  351. agency = Agency(code_interpreter_agent, user_context=None)
  352. # Test the simple usage of the code interpreter tool (answer is always 37)
  353. # Use a deterministic script to avoid RNG differences across environments
  354. question = """
  355. Use CodeInterpreter tool to execute this script and tell me the results:
  356. ```print(sum([10, 20, 7]))```
  357. """
  358. response_result = await agency.get_response(question)
  359. # Verify response
  360. assert response_result is not None
  361. assert "37" in response_result.final_output.lower()
  362. # Execute python script (answer is always 14910)
  363. query = "Run test-python script, return me its results and tell me exactly what you did to get them."
  364. response_result = await agency.get_response(query)
  365. assert response_result is not None
  366. # Handle various number formatting (with/without commas, LaTeX formatting, etc.)
  367. response_text = response_result.final_output.lower()
  368. # Remove LaTeX formatting and common separators to find the core number
  369. numbers_in_response = re.findall(r"14[,\s\\()]*910", response_text)
  370. assert len(numbers_in_response) > 0, (
  371. f"Expected to find '14910' (possibly formatted) in response. Response: {response_result.final_output}"
  372. )
  373. finally:
  374. # Cleanup: Delete uploaded file from OpenAI and temp directory
  375. try:
  376. for file in folder_path.glob("*"):
  377. file_id = code_interpreter_agent.file_manager.get_id_from_file(file)
  378. if file_id:
  379. await real_openai_client.files.delete(file_id=file_id)
  380. print(f"Cleaned up file {file.name}")
  381. os.remove(file)
  382. vector_store_id = folder_path.name.split("_vs_")[-1]
  383. await real_openai_client.vector_stores.delete(vector_store_id=f"vs_{vector_store_id}")
  384. print(f"Cleaned up vector store {folder_path.name}")
  385. os.rmdir(folder_path)
  386. print(f"Cleaned up folder {folder_path.name}")
  387. # Clean up the tmp directory if it's empty
  388. if tmp_dir.exists() and not any(tmp_dir.iterdir()):
  389. os.rmdir(tmp_dir)
  390. print(f"Cleaned up tmp directory {tmp_dir}")
  391. except Exception as e:
  392. print(f"Error cleaning up: {e}, dir: {tmp_dir.glob('*')}")
  393. @pytest.mark.asyncio
  394. async def test_agent_vision_capabilities(real_openai_client: AsyncOpenAI, tmp_path: Path):
  395. """
  396. Tests that an agent can process images using OpenAI's vision capabilities.
  397. Uses the input_image format with base64 encoded images.
  398. Uses the pre-generated example images since vision requires actual image files.
  399. """
  400. def image_to_base64(image_path: Path) -> str:
  401. """Convert image file to base64 string."""
  402. with open(image_path, "rb") as image_file:
  403. encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
  404. return encoded_string
  405. # Use the example images since they're actual image files (not text files)
  406. # Resolve paths relative to the project root
  407. project_root = Path(__file__).resolve().parents[3] # Go up to project root
  408. test_images = [
  409. (
  410. project_root / "examples/data/shapes_and_text.png",
  411. "How many shapes do you see in this image?",
  412. ("three", "3"),
  413. ),
  414. (project_root / "examples/data/shapes_and_text.png", "What text do you see in this image?", "VISION TEST 2024"),
  415. ]
  416. # Verify test images exist
  417. for image_path, _, _ in test_images:
  418. assert image_path.exists(), f"Test image not found at {image_path}"
  419. # Create a vision-capable agent with temperature=0 for deterministic responses
  420. vision_agent = Agent(
  421. name="VisionAgent",
  422. instructions="""You are an expert vision AI that can analyze images accurately.
  423. When images are provided, examine them carefully and answer questions about their content.
  424. Be precise and specific in your descriptions.""",
  425. model_settings=ModelSettings(temperature=0.0),
  426. )
  427. vision_agent._openai_client = real_openai_client
  428. # Initialize agency for the agent
  429. Agency(vision_agent, user_context=None)
  430. # Test processing each image
  431. for image_path, question, expected_content in test_images:
  432. print(f"\nTesting vision processing of {image_path.name}")
  433. # Convert image to base64
  434. b64_image = image_to_base64(image_path)
  435. # Create message with input_image format
  436. message_with_image = [
  437. {
  438. "role": "user",
  439. "content": [
  440. {
  441. "type": "input_image",
  442. "detail": "auto",
  443. "image_url": f"data:image/png;base64,{b64_image}",
  444. }
  445. ],
  446. },
  447. {"role": "user", "content": question},
  448. ]
  449. # Process the image - OpenAI will automatically handle vision processing
  450. response_result = await vision_agent.get_response(message_with_image)
  451. # Verify response
  452. assert response_result is not None
  453. assert response_result.final_output is not None
  454. print(f"Vision response for {image_path.name}: {response_result.final_output}")
  455. # Use case-insensitive search for matching (accept any alternative)
  456. response_lower = response_result.final_output.lower()
  457. alternatives = (expected_content,) if isinstance(expected_content, str) else expected_content
  458. content_found = any(alt.lower() in response_lower for alt in alternatives)
  459. assert content_found, (
  460. f"Expected content '{expected_content}' not found in vision response for {image_path.name}. "
  461. f"This suggests the vision processing failed or the model couldn't see the image content. "
  462. f"Response: {response_result.final_output}"
  463. )
  464. # Verify NO custom tool calls were made (OpenAI processes vision automatically)
  465. tool_calls_found = False
  466. for item in response_result.new_items:
  467. if isinstance(item, ToolCallItem):
  468. tool_calls_found = True
  469. print(f"Unexpected tool call found for {image_path.name}: {item.raw_item}")
  470. # We expect NO tool calls since OpenAI processes vision automatically
  471. assert not tool_calls_found, (
  472. f"No tool calls should be found for {image_path.name} since OpenAI automatically processes vision. "
  473. f"The presence of tool calls suggests the implementation is incorrectly trying to use custom tools."
  474. )
  475. @pytest.mark.asyncio
  476. @pytest.mark.skipif(
  477. os.getenv("CI") == "true",
  478. reason="Requires live OpenAI API; skipped on CI to avoid upstream flake.",
  479. )
  480. async def test_vector_store_cleanup_on_init(real_openai_client: AsyncOpenAI, tmp_path: Path):
  481. """Agent initialization synchronizes vector store with local files, removing orphaned files from VS and OpenAI."""
  482. source_file = Path("tests/data/files/favorite_books.txt")
  483. assert source_file.exists(), f"Test file not found at {source_file}"
  484. # Create temp folder with two files
  485. files_dir = tmp_path / "cleanup_files"
  486. files_dir.mkdir(exist_ok=True)
  487. file_a = files_dir / "books_a.txt"
  488. file_b = files_dir / "books_b.txt"
  489. file_a.write_text(source_file.read_text(encoding="utf-8"), encoding="utf-8")
  490. file_b.write_text(source_file.read_text(encoding="utf-8"), encoding="utf-8")
  491. agent_kwargs = {
  492. "name": "CleanupAgent",
  493. "instructions": "Use FileSearch to answer from documents.",
  494. "files_folder": str(files_dir),
  495. "include_search_results": True,
  496. "model": "gpt-5.4-mini",
  497. "model_settings": ModelSettings(tool_choice="file_search"),
  498. "tool_use_behavior": "stop_on_first_tool",
  499. }
  500. # First init: uploads both files and creates VS
  501. agent1 = Agent(**agent_kwargs)
  502. agent1._openai_client = real_openai_client
  503. Agency(agent1, user_context=None)
  504. await _wait_for_vector_store(real_openai_client, agent1)
  505. # Find VS folder and collect uploaded ids
  506. candidates = list(files_dir.parent.glob(f"{files_dir.name}_vs_*"))
  507. folder_path = candidates[0] if candidates else None
  508. assert folder_path and folder_path.exists(), "No vector store folder found"
  509. uploaded_ids = []
  510. for f in folder_path.glob("*"):
  511. if f.is_file():
  512. fid = agent1.file_manager.get_id_from_file(f)
  513. if fid:
  514. uploaded_ids.append(fid)
  515. assert len(uploaded_ids) == 2, f"Expected 2 uploaded files, got {len(uploaded_ids)}"
  516. # Remove one local file
  517. local_files = [p for p in folder_path.glob("*") if p.is_file()]
  518. assert len(local_files) >= 2
  519. removed_local = local_files[0]
  520. removed_id = agent1.file_manager.get_id_from_file(removed_local)
  521. os.remove(removed_local)
  522. # Re-init: should detach removed from VS and delete OpenAI file object
  523. agent2 = Agent(**agent_kwargs)
  524. agent2._openai_client = real_openai_client
  525. Agency(agent2, user_context=None)
  526. await _wait_for_vector_store(real_openai_client, agent2)
  527. vs_id = agent2._associated_vector_store_id
  528. assert isinstance(vs_id, str) and vs_id
  529. # Vector Store file listings are eventually consistent; do not assert immediate absence here.
  530. await _assert_openai_file_absent(real_openai_client, removed_id)
  531. # Cleanup
  532. try:
  533. await _cleanup_file_search_resources(real_openai_client, folder_path, agent2)
  534. except Exception as e:
  535. print(f"Cleanup failed: {e}")
  536. @pytest.mark.asyncio
  537. @pytest.mark.skipif(
  538. os.getenv("CI") == "true",
  539. reason="Requires live OpenAI API; skipped on CI to avoid upstream flake.",
  540. )
  541. async def test_file_reupload_on_mtime_update(real_openai_client: AsyncOpenAI, tmp_path: Path):
  542. """Modifying local file triggers re-upload with a new file_id and VS update."""
  543. source_file = Path("tests/data/files/favorite_books.txt")
  544. assert source_file.exists(), f"Test file not found at {source_file}"
  545. # Create temp folder and copy file
  546. files_dir = tmp_path / "reupload_files"
  547. files_dir.mkdir(exist_ok=True)
  548. local_file = files_dir / "favorite_books.txt"
  549. shutil.copy2(source_file, local_file)
  550. agent_kwargs = {
  551. "name": "ReuploadAgent",
  552. "instructions": "Use FileSearch to answer from documents.",
  553. "files_folder": str(files_dir),
  554. "include_search_results": True,
  555. "model": "gpt-5.4-mini",
  556. "model_settings": ModelSettings(tool_choice="file_search"),
  557. "tool_use_behavior": "stop_on_first_tool",
  558. }
  559. # First init: upload original file
  560. agent1 = Agent(**agent_kwargs)
  561. agent1._openai_client = real_openai_client
  562. Agency(agent1, user_context=None)
  563. await _wait_for_vector_store(real_openai_client, agent1)
  564. # Locate vector store folder and uploaded file id
  565. candidates = list(files_dir.parent.glob(f"{files_dir.name}_vs_*"))
  566. folder_path = candidates[0] if candidates else None
  567. assert folder_path and folder_path.exists(), "No vector store folder found"
  568. vs_files_local = [p for p in folder_path.glob("*") if p.is_file()]
  569. assert len(vs_files_local) == 1
  570. uploaded_path = vs_files_local[0]
  571. old_id = agent1.file_manager.get_id_from_file(uploaded_path)
  572. assert isinstance(old_id, str) and old_id
  573. # Ensure mtime > created_at by waiting and modifying the file
  574. time.sleep(2)
  575. with open(uploaded_path, "a", encoding="utf-8") as f:
  576. f.write("\nReupload test line.")
  577. # Bump mtime explicitly to avoid rounding issues
  578. try:
  579. st = uploaded_path.stat()
  580. os.utime(uploaded_path, (st.st_atime, st.st_mtime + 2))
  581. except Exception:
  582. pass
  583. # Re-init agent: should detect newer mtime and re-upload
  584. agent2 = Agent(**agent_kwargs)
  585. agent2._openai_client = real_openai_client
  586. Agency(agent2, user_context=None)
  587. await _wait_for_vector_store(real_openai_client, agent2)
  588. vs_id = agent2._associated_vector_store_id
  589. assert isinstance(vs_id, str) and vs_id
  590. # Verify that reupload occurred (new file was uploaded)
  591. # Note: We don't test that the old file was removed from the vector store,
  592. # as vector store cleanup may be eventually consistent
  593. vs_files = await real_openai_client.vector_stores.files.list(vector_store_id=vs_id, filter="completed")
  594. new_ids = {getattr(f, "file_id", None) or getattr(f, "id", None) for f in vs_files.data}
  595. assert len(new_ids) >= 1, f"Expected at least 1 file in vector store, got {len(new_ids)}"
  596. # Verify that a new file ID exists (reupload occurred) - either old file is gone or we have multiple files
  597. assert old_id not in new_ids or len(new_ids) > 1, (
  598. f"Reupload should create new file, but only found old_id {old_id} in {new_ids}"
  599. )
  600. # Cleanup
  601. try:
  602. await _cleanup_file_search_resources(real_openai_client, folder_path, agent2)
  603. except Exception as e:
  604. print(f"Cleanup failed: {e}")