|
15 | 15 | from openai.types.beta.threads.run import RequiredAction
|
16 | 16 | from openai.types.beta.threads.message_content_delta import MessageContentDelta
|
17 | 17 | from openai.types.beta.threads.text_delta_block import TextDeltaBlock
|
| 18 | +from openai.types.beta.threads.text_delta import TextDelta |
18 | 19 | from fastapi.responses import StreamingResponse
|
19 | 20 | from fastapi import APIRouter, Depends, Form
|
20 | 21 |
|
21 | 22 | import json
|
22 | 23 |
|
23 |
| -from routers import files as files_router |
| 24 | +from routers.files import router as files_router |
24 | 25 | from utils.custom_functions import get_weather, post_tool_outputs
|
25 | 26 | from utils.sse import sse_format
|
26 | 27 | from utils.streaming import AssistantStreamMetadata
|
@@ -123,37 +124,60 @@ async def handle_assistant_stream(
|
123 | 124 | )
|
124 | 125 |
|
125 | 126 | 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: |
128 | 129 | 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 | + |
133 | 137 | if annotations:
|
134 |
| - logger.debug(f"Annotation found: {annotations}") |
135 | 138 | for annotation in annotations:
|
| 139 | + # Handle file_citation (user-uploaded files for retrieval tool) |
136 | 140 | 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}'") |
144 | 153 | 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) |
157 | 181 |
|
158 | 182 | if isinstance(event, ThreadRunStepCreated) and event.data.type == "tool_calls":
|
159 | 183 | logger.debug(f"Tool Call Created - Data: {str(event.data)}")
|
@@ -191,14 +215,12 @@ async def handle_assistant_stream(
|
191 | 215 | # Handle code interpreter tool calls
|
192 | 216 | elif tool_call.type == "code_interpreter":
|
193 | 217 | if tool_call.code_interpreter and tool_call.code_interpreter.input:
|
194 |
| - logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}") |
195 | 218 | yield sse_format(
|
196 | 219 | "toolDelta",
|
197 | 220 | wrap_for_oob_swap(step_id, str(tool_call.code_interpreter.input))
|
198 | 221 | )
|
199 | 222 | if tool_call.code_interpreter and tool_call.code_interpreter.outputs:
|
200 | 223 | for output in tool_call.code_interpreter.outputs:
|
201 |
| - logger.debug(f"Code Interpreter Output Type: {output.type}") |
202 | 224 | if output.type == "logs" and output.logs:
|
203 | 225 | yield sse_format(
|
204 | 226 | "toolDelta",
|
|
0 commit comments