Skip to content

Commit b202752

Browse files
authored
fix: upload Thread attachments when sending messages (#1394)
## Threads ### Resolved Issues - Fixed: After the first message, sending a thread message with file search attachments may fail to send.
1 parent c3fe784 commit b202752

3 files changed

Lines changed: 103 additions & 13 deletions

File tree

pingpong/ai.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3356,6 +3356,39 @@ async def on_response_canceled(self) -> None:
33563356
)
33573357

33583358

3359+
async def poll_vector_store_files(
3360+
cli: openai.AsyncClient,
3361+
*,
3362+
vector_store_id: str,
3363+
file_ids: list[str],
3364+
) -> None:
3365+
async def poll_single_file(file_id: str) -> bool:
3366+
try:
3367+
await cli.vector_stores.files.poll(
3368+
file_id=file_id,
3369+
vector_store_id=vector_store_id,
3370+
)
3371+
return True
3372+
except openai.NotFoundError:
3373+
return False
3374+
3375+
poll_results = await asyncio.gather(
3376+
*[poll_single_file(file_id) for file_id in file_ids]
3377+
)
3378+
missing_file_ids = [
3379+
file_id
3380+
for file_id, was_found in zip(file_ids, poll_results, strict=False)
3381+
if not was_found
3382+
]
3383+
if missing_file_ids:
3384+
logger.warning(
3385+
"Skipping %s missing file(s) during vector store poll for vector store %s: %s",
3386+
len(missing_file_ids),
3387+
vector_store_id,
3388+
", ".join(missing_file_ids),
3389+
)
3390+
3391+
33593392
async def run_response(
33603393
cli: openai.AsyncClient,
33613394
*,
@@ -3463,14 +3496,10 @@ async def run_response(
34633496
if attached_file_search_file_ids:
34643497
if not thread_vector_store_id:
34653498
raise ValueError("Vector store ID is required for file search")
3466-
await asyncio.gather(
3467-
*[
3468-
cli.vector_stores.files.poll(
3469-
file_id=file_id,
3470-
vector_store_id=thread_vector_store_id,
3471-
)
3472-
for file_id in attached_file_search_file_ids
3473-
]
3499+
await poll_vector_store_files(
3500+
cli,
3501+
vector_store_id=thread_vector_store_id,
3502+
file_ids=attached_file_search_file_ids,
34743503
)
34753504
if vector_store_ids:
34763505
tools.append(

pingpong/server.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
from .state_types import AppState, StateRequest, StateWebSocket
146146
from .vector_stores import (
147147
add_vector_store_files_to_db,
148+
append_vector_store_files,
148149
create_vector_store,
149150
delete_vector_store,
150151
delete_vector_store_db_returning_file_ids,
@@ -6267,11 +6268,19 @@ async def send_message(
62676268
if data.file_search_file_ids:
62686269
if thread.vector_store_id:
62696270
# Vector store already exists, update
6270-
await add_vector_store_files_to_db(
6271-
request.state["db"],
6272-
thread.vector_store_id,
6273-
data.file_search_file_ids,
6274-
)
6271+
if thread.version == 3:
6272+
await append_vector_store_files(
6273+
request.state["db"],
6274+
openai_client,
6275+
thread.vector_store_id,
6276+
data.file_search_file_ids,
6277+
)
6278+
else:
6279+
await add_vector_store_files_to_db(
6280+
request.state["db"],
6281+
thread.vector_store_id,
6282+
data.file_search_file_ids,
6283+
)
62756284
else:
62766285
# Store doesn't exist, create a new one
62776286
# (empty, since we're adding files as attachments)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import logging
2+
from unittest.mock import AsyncMock
3+
4+
import httpx
5+
import openai
6+
import pytest
7+
8+
from pingpong.ai import poll_vector_store_files
9+
10+
pytestmark = pytest.mark.asyncio
11+
12+
13+
def _not_found_error(file_id: str, vector_store_id: str) -> openai.NotFoundError:
14+
request = httpx.Request(
15+
"GET",
16+
f"https://api.openai.com/v1/vector_stores/{vector_store_id}/files/{file_id}",
17+
)
18+
response = httpx.Response(404, request=request)
19+
return openai.NotFoundError(
20+
f"No file found with id '{file_id}' in vector store '{vector_store_id}'.",
21+
response=response,
22+
body={"error": {"message": "not found"}},
23+
)
24+
25+
26+
async def test_poll_vector_store_files_skips_missing_files(caplog):
27+
cli = AsyncMock()
28+
29+
async def fake_poll(*, file_id: str, vector_store_id: str):
30+
if file_id == "file-missing":
31+
raise _not_found_error(file_id, vector_store_id)
32+
return None
33+
34+
cli.vector_stores.files.poll = AsyncMock(side_effect=fake_poll)
35+
36+
with caplog.at_level(logging.WARNING):
37+
await poll_vector_store_files(
38+
cli, vector_store_id="vs-test", file_ids=["file-ok", "file-missing"]
39+
)
40+
41+
assert cli.vector_stores.files.poll.await_count == 2
42+
assert "file-missing" in caplog.text
43+
assert "vs-test" in caplog.text
44+
45+
46+
async def test_poll_vector_store_files_noop_for_empty_file_list():
47+
cli = AsyncMock()
48+
cli.vector_stores.files.poll = AsyncMock()
49+
50+
await poll_vector_store_files(cli, vector_store_id="vs-test", file_ids=[])
51+
52+
assert cli.vector_stores.files.poll.await_count == 0

0 commit comments

Comments
 (0)