Skip to content

Commit e782bc9

Browse files
Replace Code Interpreter file links with links to our application API for OpenAI file download
1 parent cdb5c7a commit e782bc9

File tree

5 files changed

+265
-237
lines changed

5 files changed

+265
-237
lines changed

routers/chat.py

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
from openai.types.beta.threads.run import RequiredAction
1616
from openai.types.beta.threads.message_content_delta import MessageContentDelta
1717
from openai.types.beta.threads.text_delta_block import TextDeltaBlock
18+
from openai.types.beta.threads.text_delta import TextDelta
1819
from fastapi.responses import StreamingResponse
1920
from fastapi import APIRouter, Depends, Form
2021

2122
import json
2223

23-
from routers import files as files_router
24+
from routers.files import router as files_router
2425
from utils.custom_functions import get_weather, post_tool_outputs
2526
from utils.sse import sse_format
2627
from utils.streaming import AssistantStreamMetadata
@@ -123,37 +124,60 @@ async def handle_assistant_stream(
123124
)
124125

125126
if isinstance(event, ThreadMessageDelta) and event.data.delta.content:
126-
content: MessageContentDelta = event.data.delta.content[0]
127-
if isinstance(content, TextDeltaBlock) and content.text and content.text.value:
127+
delta_content_item: MessageContentDelta = event.data.delta.content[0]
128+
if isinstance(delta_content_item, TextDeltaBlock) and delta_content_item.text:
128129
step_id = event.data.id
129-
text_value = content.text.value
130-
annotations = content.text.annotations
131-
132-
# Check for file citation annotations
130+
text_delta: TextDelta = delta_content_item.text
131+
current_delta_text_value: Optional[str] = text_delta.value
132+
annotations = text_delta.annotations
133+
134+
# This will be the text actually sent in textDelta
135+
final_text_for_this_delta = current_delta_text_value
136+
133137
if annotations:
134-
logger.debug(f"Annotation found: {annotations}")
135138
for annotation in annotations:
139+
# Handle file_citation (user-uploaded files for retrieval tool)
136140
if annotation.type == 'file_citation' and hasattr(annotation, 'file_citation') and annotation.file_citation:
137-
match = re.search(r'【.*?†(.*?)】', text_value)
138-
if match:
139-
file_name = match.group(1)
140-
# Manually construct the URL
141-
file_url = f'/assistants/{assistant_id}/files/{file_name}'
142-
text_value = f'[†]({file_url})'
143-
logger.debug(f"Replacing citation with link: {text_value}")
141+
# Replace the file citation placeholder with our application's download URL for the cited file
142+
if current_delta_text_value:
143+
match = re.search(r'【.*?†(.*?)】', current_delta_text_value)
144+
if match:
145+
file_name_in_citation = match.group(1)
146+
# URL for user-uploaded files, served by filename by our app
147+
file_url = files_router.url_path_for('download_assistant_file', assistant_id=assistant_id, file_name=file_name_in_citation)
148+
# Replace the placeholder within this delta's text value
149+
final_text_for_this_delta = current_delta_text_value.replace(match.group(0), f'[†]({file_url})')
150+
logger.debug(f"Replaced file citation placeholder in delta with link: {final_text_for_this_delta}")
151+
else:
152+
logger.warning(f"File citation annotation present, but pattern not found in delta text: '{current_delta_text_value}'")
144153
else:
145-
# Handle error: pattern not found in the text
146-
logger.warning(f"Could not extract filename from citation text: {text_value}")
147-
file_name = None # Indicate failure
148-
149-
# Assuming one citation per delta for now
150-
break
151-
152-
sse_data = wrap_for_oob_swap(step_id, text_value)
153-
yield sse_format(
154-
"textDelta",
155-
sse_data
156-
)
154+
# This case shouldn't occur
155+
logger.warning(f"File citation annotation found, but text_delta.value is unexpectedly None.")
156+
157+
# Handle file_path (code interpreter generated files)
158+
elif annotation.type == 'file_path' and hasattr(annotation, 'file_path') and annotation.file_path and annotation.file_path.file_id:
159+
file_id = annotation.file_path.file_id
160+
# annotation.text is the "key" for replacement (e.g., "sandbox:/mnt/data/file.csv")
161+
sandbox_link_text_in_markdown = annotation.text
162+
163+
# We will replace it with our app's download URL for the OpenAI-hosted file
164+
download_url = files_router.url_path_for(
165+
'download_openai_file', assistant_id=assistant_id, file_id=file_id
166+
)
167+
168+
replacement_payload = f"{sandbox_link_text_in_markdown}|{download_url}"
169+
# Use step_id (message_id) for OOB targeting the correct message container
170+
sse_replacement_data = wrap_for_oob_swap(step_id, replacement_payload)
171+
yield sse_format("textReplacement", sse_replacement_data)
172+
logger.debug(f"Sent textReplacement event for {sandbox_link_text_in_markdown} with {download_url}")
173+
174+
break
175+
176+
# Only send SSE if there's a non-None text value to transmit
177+
if final_text_for_this_delta is not None:
178+
# Use step_id (message_id) for OOB targeting the correct message container
179+
sse_data = wrap_for_oob_swap(step_id, final_text_for_this_delta)
180+
yield sse_format("textDelta", sse_data)
157181

158182
if isinstance(event, ThreadRunStepCreated) and event.data.type == "tool_calls":
159183
logger.debug(f"Tool Call Created - Data: {str(event.data)}")
@@ -191,14 +215,12 @@ async def handle_assistant_stream(
191215
# Handle code interpreter tool calls
192216
elif tool_call.type == "code_interpreter":
193217
if tool_call.code_interpreter and tool_call.code_interpreter.input:
194-
logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}")
195218
yield sse_format(
196219
"toolDelta",
197220
wrap_for_oob_swap(step_id, str(tool_call.code_interpreter.input))
198221
)
199222
if tool_call.code_interpreter and tool_call.code_interpreter.outputs:
200223
for output in tool_call.code_interpreter.outputs:
201-
logger.debug(f"Code Interpreter Output Type: {output.type}")
202224
if output.type == "logs" and output.logs:
203225
yield sse_format(
204226
"toolDelta",

routers/files.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,22 +215,24 @@ async def delete_file(
215215

216216
# --- Streaming file content routes ---
217217

218+
218219
@router.get("/{file_name}")
219220
async def download_assistant_file(
220221
assistant_id: str = Path(..., description="The ID of the Assistant"),
221222
file_name: str = Path(..., description="The name of the file to retrieve")
222223
) -> FileResponse:
223-
"""Serves an assistant file stored locally in the uploads directory."""
224+
"""This endpoint retrieves files uploaded TO openai as file search inputs
225+
and stored locally in the uploads directory (since OpenAI doesn't serve
226+
them for download)."""
224227
return retrieve_file(file_name)
225228

226229

227-
# This endpoint retrieves files uploaded TO openai (e.g., code interpreter output)
228-
# Keep it separate for clarity
229230
@router.get("/{file_id}/openai_content")
230231
async def download_openai_file(
231232
file_id: str = Path(..., description="The ID of the file stored in OpenAI"),
232233
client: AsyncOpenAI = Depends(lambda: AsyncOpenAI())
233234
) -> StreamingResponse:
235+
"""This endpoint retrieves files created by the code interpreter"""
234236
try:
235237
file = await client.files.retrieve(file_id)
236238
file_content = await client.files.content(file_id)
@@ -241,7 +243,7 @@ async def download_openai_file(
241243
# Use stream_file_content helper
242244
return StreamingResponse(
243245
stream_file_content(file_content.content), # Assuming stream_file_content handles bytes
244-
headers={"Content-Disposition": f'attachment; filename="{file.filename or file_id}"'} # Use file_id as fallback filename
246+
headers={"Content-Disposition": f'attachment; filename="{file.filename or file_id}"'}
245247
)
246248
except Exception as e:
247249
logger.error(f"Error downloading file {file_id} from OpenAI: {e}")

0 commit comments

Comments
 (0)