-
Notifications
You must be signed in to change notification settings - Fork 10
fix(collections): Use correct file batch ID #1073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aea2dfa
6ca053f
8c02d57
d391ad2
3f6fc81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| import json | ||
| import logging | ||
| import time | ||
| import functools as ft | ||
|
|
||
| import openai | ||
| from openai import OpenAI, OpenAIError | ||
| from openai.types import VectorStore | ||
| from openai.types.vector_stores import VectorStoreFileBatch | ||
| from pydantic import BaseModel | ||
|
|
||
| from app.models import Document, ProviderType | ||
|
|
@@ -12,6 +15,15 @@ | |
|
|
||
| OPENAI_PROVIDER = ProviderType.openai.value | ||
|
|
||
| OPENAI_MAX_RETRIES = 2 | ||
|
|
||
| # Per socket operation, not per call: a steady upload runs as long as it needs and | ||
| # this only fires on a dead connection. Sized so all attempts fit one Celery task. | ||
| OPENAI_TIMEOUT_SECONDS = 90 | ||
|
|
||
| BATCH_POLL_INTERVAL_SECONDS = 2 | ||
| BATCH_POLL_TIMEOUT_SECONDS = 240 | ||
|
|
||
|
|
||
| def vs_ls(client: OpenAI, vector_store_id: str): | ||
| kwargs = {} | ||
|
|
@@ -85,7 +97,7 @@ def __init__(self, client): | |
|
|
||
|
|
||
| class OpenAIVectorStoreCrud(OpenAICrud): | ||
| def create(self): | ||
| def create(self) -> VectorStore: | ||
| logger.info( | ||
| f"[OpenAIVectorStoreCrud.create] Creating vector store | {{'action': 'create'}}" | ||
| ) | ||
|
|
@@ -101,6 +113,52 @@ def read(self, vector_store_id: str): | |
| ) | ||
| yield from vs_ls(self.client, vector_store_id) | ||
|
|
||
| def _create_file_batch(self, vector_store_id: str, file_ids: list[str]) -> str: | ||
| """Returns the vsfb_ id. poll()'s return deserializes a vector-store body, | ||
| so its .id is the vs_ id - take the batch id from create().""" | ||
| created = self.client.vector_stores.file_batches.create( | ||
| vector_store_id=vector_store_id, | ||
| file_ids=file_ids, | ||
| ) | ||
| return created.id | ||
|
|
||
| def _retrieve_file_batch( | ||
| self, batch_id: str, vector_store_id: str | ||
| ) -> VectorStoreFileBatch: | ||
| return self.client.vector_stores.file_batches.retrieve( | ||
| batch_id, vector_store_id=vector_store_id | ||
| ) | ||
|
|
||
| def _poll_file_batch( | ||
| self, batch_id: str, vector_store_id: str | ||
| ) -> VectorStoreFileBatch: | ||
| """Wait for indexing to finish. The SDK's own poll() loops forever.""" | ||
| deadline = time.monotonic() + BATCH_POLL_TIMEOUT_SECONDS | ||
|
|
||
| while True: | ||
| batch = self._retrieve_file_batch(batch_id, vector_store_id) | ||
| if batch.status != "in_progress": | ||
| return batch | ||
|
Comment on lines
+140
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Files matching open_ai.py:"
fd -a 'open_ai\.py$' . || true
file="$(fd 'open_ai\.py$' backend/app/crud/rag 2>/dev/null | head -n1 || true)"
echo "Selected file: $file"
if [ -n "${file:-}" ]; then
wc -l "$file"
ast-grep outline "$file" --view compact || true
echo "---- lines 1-240 ----"
sed -n '1,240p' "$file" | nl -ba
fi
echo "---- related references to status/file_counts/cancelled in file ----"
if [ -n "${file:-}" ]; then
rg -n 'status|file_counts|cancel|cancelled|fail|in_progress|completed' "$file"
fiRepository: ProjectTech4DevAI/kaapi-backend Length of output: 747 🌐 Web query:
💡 Result: In the OpenAI API, both Vector Store File Batches and individual Vector Store Files share the same four possible statuses: in_progress, completed, cancelled, and failed [1][2][3][4]. When working with Vector Store File Batches, the status reflects the overall state of the batch operation [1][5]. Additionally, these objects include a file_counts attribute that provides a breakdown of the constituent files' statuses, including the number of files currently in progress, completed, failed, and cancelled, as well as the total count [1][6]. When listing files within a specific batch, you can filter the results using these same four status values [3][7]. The status completed specifically indicates that a file is successfully processed and ready for use [3][4][7]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
file="backend/app/crud/rag/open_ai.py"
echo "---- ast-grep outline ----"
ast-grep outline "$file" --view names || true
echo "---- lines 1-240 ----"
awk '{printf "%5d\t%s\n", NR, $0}' "$file" | sed -n '1,240p'
echo "---- relevant references ----"
grep -nE 'status|file_counts|cancel|cancelled|fail|in_progress|completed|update\(' "$file" || true
test_file="backend/app/tests/crud/rag/test_open_ai.py"
if [ -f "$test_file" ]; then
echo "---- tests relevant references ----"
grep -nE 'status|file_counts|cancel|cancelled|fail|in_progress|completed|update\(' "$test_file" || true
fiRepository: ProjectTech4DevAI/kaapi-backend Length of output: 14994 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="backend/app/crud/rag/open_ai.py"
echo "---- lines 240-335 ----"
awk '{printf "%5d\t%s\n", NR, $0}' "$file" | sed -n '240,335p'
echo "---- file-count-like usages repository-wide ----"
grep -RInE 'file_counts|cancelled|cancelled_files|list_files\(.*filter=' backend/app tests 2>/dev/null || trueRepository: ProjectTech4DevAI/kaapi-backend Length of output: 8174 Handle non-completed terminal batch statuses explicitly.
🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
| if time.monotonic() >= deadline: | ||
| error_message = ( | ||
| f"[KAAPI] Timed out (code: batch-poll-timeout) after " | ||
| f"{BATCH_POLL_TIMEOUT_SECONDS}s waiting for OpenAI to finish indexing " | ||
| f"{batch.file_counts.in_progress} file(s). Retry the " | ||
| f"collection, or split it into smaller batches." | ||
| ) | ||
| logger.error( | ||
| f"[OpenAIVectorStoreCrud._poll_file_batch] {error_message} | " | ||
| f"vector_store_id={vector_store_id}, batch_id={batch_id}, " | ||
| f"completed={batch.file_counts.completed}, " | ||
| f"in_progress={batch.file_counts.in_progress}" | ||
| ) | ||
| raise InterruptedError(error_message) | ||
|
|
||
| time.sleep( | ||
| min(BATCH_POLL_INTERVAL_SECONDS, max(0.0, deadline - time.monotonic())) | ||
| ) | ||
|
|
||
| def update( | ||
| self, | ||
| vector_store_id: str, | ||
|
|
@@ -115,11 +173,10 @@ def update( | |
| ) | ||
|
|
||
| try: | ||
| batch = self.client.vector_stores.file_batches.upload_and_poll( | ||
| vector_store_id=vector_store_id, | ||
| files=[], | ||
| file_ids=[doc.file_id[OPENAI_PROVIDER] for doc in docs], | ||
| batch_id = self._create_file_batch( | ||
| vector_store_id, [doc.file_id[OPENAI_PROVIDER] for doc in docs] | ||
| ) | ||
| batch = self._poll_file_batch(batch_id, vector_store_id) | ||
| except openai.RateLimitError as e: | ||
| error_message = ( | ||
| f"[OPENAI] Rate limit exceeded (code: {e.status_code}): " | ||
|
|
@@ -215,14 +272,14 @@ def update( | |
|
|
||
| logger.info( | ||
| f"[OpenAIVectorStoreCrud.update] Batch complete | " | ||
| f"{{'vector_store_id': '{vector_store_id}', " | ||
| f"{{'vector_store_id': '{vector_store_id}', 'batch_id': '{batch_id}', " | ||
| f"'completed': {batch.file_counts.completed}, 'failed': {batch.file_counts.failed}}}" | ||
| ) | ||
| if batch.file_counts.failed > 0: | ||
| try: | ||
| failed_files = self.client.vector_stores.file_batches.list_files( | ||
| vector_store_id=vector_store_id, | ||
| batch_id=batch.id, | ||
| batch_id=batch_id, | ||
| filter="failed", | ||
| ) | ||
| doc_by_file_id = {d.file_id[OPENAI_PROVIDER]: d for d in docs} | ||
|
|
@@ -232,11 +289,15 @@ def update( | |
| label = d.fname if d else f.id | ||
| msg = f.last_error.message if f.last_error else "no error detail" | ||
| parts.append(f"{label}: {msg}") | ||
| logger.error( | ||
| f"[OpenAIVectorStoreCrud.update] Files failed to index | " | ||
| f"{{'batch_id': '{batch_id}', 'failed_files': '{', '.join(parts)}'}}" | ||
| ) | ||
| raise RuntimeError("; ".join(parts)) | ||
| except OpenAIError as err: | ||
| logger.warning( | ||
| f"[OpenAIVectorStoreCrud.update] Could not fetch per-file errors | " | ||
| f"{{'batch_id': '{batch.id}', 'error': '{str(err)}'}}" | ||
| f"{{'batch_id': '{batch_id}', 'error': '{str(err)}'}}" | ||
| ) | ||
| raise | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 1362
🏁 Script executed:
Repository: ProjectTech4DevAI/kaapi-backend
Length of output: 35059
🌐 Web query:
OpenAI Python SDK configuration 90 second timeout max_retries=2 default retry behavior💡 Result:
To configure the OpenAI Python SDK with a 90-second timeout and 2 retries, you can set these parameters when initializing the OpenAI client [1][2]. Configuration Example: from openai import OpenAI import httpx # Configure timeout and retries client = OpenAI( timeout=90.0, # Set total timeout to 90 seconds [1][3] max_retries=2, # Set maximum number of retries [1][3]) Default Behavior and Details: - Timeout: The default timeout is 10 minutes (600 seconds) [4]. Setting timeout=90.0 overrides this global default [1][2]. You can also pass an httpx.Timeout object for more granular control (e.g., specifying connect, read, or write timeouts) [1][5]. - Retries: The default number of retries is already 2 [1][6]. These retries occur automatically for specific error types, including connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal Server Errors [1][3][6]. - Per-Request Overrides: If you need to apply different settings to a specific call rather than the entire client, you can use the with_options method [1][7]: response = client.with_options(timeout=90.0, max_retries=2).chat.completions.create(...) Note: Requests that time out are subject to the same retry logic and will be retried up to the configured max_retries limit [1][2][5].
Citations:
Make the batch-poll deadline enforceable.
_retrieve_file_batch()uses the client’s 90s timeout and default retries, but_poll_file_batch()only checks the 240s deadline after_retrieve_file_batch()returns. If the SDK retries timed-out retrievals, polling can exceed the advertised wall-clock budget. Bound each retrieve/retry by the remaining deadline, or disable/limit retries for polling specifically.🤖 Prompt for AI Agents
Source: MCP tools