@@ -221,20 +221,75 @@ async def _cleanup_expired_file_batches(self) -> None:
221
221
if expired_count > 0 :
222
222
logger .info (f"Cleaned up { expired_count } expired file batches" )
223
223
224
+ async def _get_completed_files_in_batch (self , vector_store_id : str , file_ids : list [str ]) -> set [str ]:
225
+ """Determine which files in a batch are actually completed by checking vector store file_ids."""
226
+ if vector_store_id not in self .openai_vector_stores :
227
+ return set ()
228
+
229
+ store_info = self .openai_vector_stores [vector_store_id ]
230
+ completed_files = set (file_ids ) & set (store_info ["file_ids" ])
231
+ return completed_files
232
+
233
+ async def _analyze_batch_completion_on_resume (self , batch_id : str , batch_info : dict [str , Any ]) -> list [str ]:
234
+ """Analyze batch completion status and return remaining files to process.
235
+
236
+ Returns:
237
+ List of file IDs that still need processing. Empty list if batch is complete.
238
+ """
239
+ vector_store_id = batch_info ["vector_store_id" ]
240
+ all_file_ids = batch_info ["file_ids" ]
241
+
242
+ # Find files that are actually completed
243
+ completed_files = await self ._get_completed_files_in_batch (vector_store_id , all_file_ids )
244
+ remaining_files = [file_id for file_id in all_file_ids if file_id not in completed_files ]
245
+
246
+ completed_count = len (completed_files )
247
+ total_count = len (all_file_ids )
248
+ remaining_count = len (remaining_files )
249
+
250
+ # Update file counts to reflect actual state
251
+ batch_info ["file_counts" ] = {
252
+ "completed" : completed_count ,
253
+ "failed" : 0 , # We don't track failed files during resume - they'll be retried
254
+ "in_progress" : remaining_count ,
255
+ "cancelled" : 0 ,
256
+ "total" : total_count ,
257
+ }
258
+
259
+ # If all files are already completed, mark batch as completed
260
+ if remaining_count == 0 :
261
+ batch_info ["status" ] = "completed"
262
+ logger .info (f"Batch { batch_id } is already fully completed, updating status" )
263
+
264
+ # Save updated batch info
265
+ await self ._save_openai_vector_store_file_batch (batch_id , batch_info )
266
+
267
+ return remaining_files
268
+
224
269
async def _resume_incomplete_batches (self ) -> None :
225
270
"""Resume processing of incomplete file batches after server restart."""
226
271
for batch_id , batch_info in self .openai_file_batches .items ():
227
272
if batch_info ["status" ] == "in_progress" :
228
- logger .info (f"Resuming incomplete file batch: { batch_id } " )
229
- # Restart the background processing task
230
- task = asyncio .create_task (self ._process_file_batch_async (batch_id , batch_info ))
231
- self ._file_batch_tasks [batch_id ] = task
273
+ logger .info (f"Analyzing incomplete file batch: { batch_id } " )
274
+
275
+ remaining_files = await self ._analyze_batch_completion_on_resume (batch_id , batch_info )
276
+
277
+ # Check if batch is now completed after analysis
278
+ if batch_info ["status" ] == "completed" :
279
+ continue
280
+
281
+ if remaining_files :
282
+ logger .info (f"Resuming batch { batch_id } with { len (remaining_files )} remaining files" )
283
+ # Restart the background processing task with only remaining files
284
+ task = asyncio .create_task (self ._process_file_batch_async (batch_id , batch_info , remaining_files ))
285
+ self ._file_batch_tasks [batch_id ] = task
232
286
233
287
async def initialize_openai_vector_stores (self ) -> None :
234
288
"""Load existing OpenAI vector stores and file batches into the in-memory cache."""
235
289
self .openai_vector_stores = await self ._load_openai_vector_stores ()
236
290
self .openai_file_batches = await self ._load_openai_vector_store_file_batches ()
237
291
self ._file_batch_tasks = {}
292
+ # TODO: Enable resume for multi-worker deployments, only works for single worker for now
238
293
await self ._resume_incomplete_batches ()
239
294
self ._last_file_batch_cleanup_time = 0
240
295
@@ -645,6 +700,14 @@ async def openai_attach_file_to_vector_store(
645
700
if vector_store_id not in self .openai_vector_stores :
646
701
raise VectorStoreNotFoundError (vector_store_id )
647
702
703
+ # Check if file is already attached to this vector store
704
+ store_info = self .openai_vector_stores [vector_store_id ]
705
+ if file_id in store_info ["file_ids" ]:
706
+ logger .warning (f"File { file_id } is already attached to vector store { vector_store_id } , skipping" )
707
+ # Return existing file object
708
+ file_info = await self ._load_openai_vector_store_file (vector_store_id , file_id )
709
+ return VectorStoreFileObject (** file_info )
710
+
648
711
attributes = attributes or {}
649
712
chunking_strategy = chunking_strategy or VectorStoreChunkingStrategyAuto ()
650
713
created_at = int (time .time ())
@@ -1022,9 +1085,10 @@ async def _process_file_batch_async(
1022
1085
self ,
1023
1086
batch_id : str ,
1024
1087
batch_info : dict [str , Any ],
1088
+ override_file_ids : list [str ] | None = None ,
1025
1089
) -> None :
1026
1090
"""Process files in a batch asynchronously in the background."""
1027
- file_ids = batch_info ["file_ids" ]
1091
+ file_ids = override_file_ids if override_file_ids is not None else batch_info ["file_ids" ]
1028
1092
attributes = batch_info ["attributes" ]
1029
1093
chunking_strategy = batch_info ["chunking_strategy" ]
1030
1094
vector_store_id = batch_info ["vector_store_id" ]
0 commit comments