diff --git a/backend/app/services/collections/providers/openai.py b/backend/app/services/collections/providers/openai.py index 770e0773f..84993268c 100644 --- a/backend/app/services/collections/providers/openai.py +++ b/backend/app/services/collections/providers/openai.py @@ -47,8 +47,14 @@ def upload_files( if doc.file_size_kb is None: doc.file_size_kb = round(tmp.tell() / 1024, 2) tmp.seek(0) + if "." in doc.fname: + filename, extension = doc.fname.rsplit(".", 1) + normalized_fname = f"{filename}.{extension.lower()}" + else: + normalized_fname = doc.fname uploaded = self.client.files.create( - file=(doc.fname, tmp), purpose="assistants" + file=(normalized_fname, tmp), + purpose="assistants", ) except Exception as err: logger.error( diff --git a/backend/app/tests/services/collections/providers/test_openai_provider.py b/backend/app/tests/services/collections/providers/test_openai_provider.py index d31fb0ae3..8d8cf42c0 100644 --- a/backend/app/tests/services/collections/providers/test_openai_provider.py +++ b/backend/app/tests/services/collections/providers/test_openai_provider.py @@ -296,15 +296,25 @@ def test_upload_files_empty_docs_is_noop() -> None: client.files.create.assert_not_called() -def test_upload_files_file_name_matches_doc_fname() -> None: - """The file tuple passed to OpenAI must carry the original filename.""" +@pytest.mark.parametrize( + ("fname", "expected"), + [ + ("report.pdf", "report.pdf"), + ("Report.PDF", "Report.pdf"), + ("README", "README"), + ], +) +def test_upload_files_file_name_lowercases_only_extension( + fname: str, expected: str +) -> None: + """The file tuple passed to OpenAI keeps the stem's case, lowercases the extension.""" client = MagicMock() client.files.create.return_value = MagicMock(id="file-abc") provider = OpenAIProvider(client=client) storage = _make_storage(b"data") doc = _make_doc(file_size_kb=1.0) - doc.fname = "report.pdf" + doc.fname = fname session_p, crud_p = _patch_session_and_crud() with session_p as MockSession, crud_p: @@ -313,8 +323,8 @@ def test_upload_files_file_name_matches_doc_fname() -> None: provider.upload_files(storage, [doc], project_id=1) _, kwargs = client.files.create.call_args - fname, _ = kwargs["file"] - assert fname == "report.pdf" + uploaded_fname, _ = kwargs["file"] + assert uploaded_fname == expected def test_upload_files_raises_on_db_update_failure() -> None: diff --git a/docs/wiki/modules/knowledge-base.md b/docs/wiki/modules/knowledge-base.md index 4cefd5719..179b0c69d 100644 --- a/docs/wiki/modules/knowledge-base.md +++ b/docs/wiki/modules/knowledge-base.md @@ -35,6 +35,7 @@ All paths relative to `backend/app/`. ## Gotchas - `signed_url` behaviour is chosen per request by the `download` query param (default false = inline, as before). `put()` stores the upload's `Content-Type`, so a bare presigned URL for a PDF opens in a tab (CSV/XLSX datasets happen to download, which is why only KB docs looked broken). `get_signed_url(..., filename=...)` adds `ResponseContentDisposition: attachment`; `_signed_url()` in `services/documents/helpers.py` passes `fname` only when `download` is set, and the flag threads through `build_document_schema(s)` / `build_job_schema(s)` from `api/routes/documents.py`, `doc_transformation_job.py` and `collections.py`. An attachment URL will not render in an `