fix: use JSON encoded SSE chunks to preserve multi-line responses and prevent stream breaking on newline chunks
Browse files- src/api/v1/chat.py +3 -2
- src/services/gemini_service.py +26 -3
src/api/v1/chat.py
CHANGED
|
@@ -127,20 +127,21 @@ async def chat_stream(request: Request, payload: ChatRequest, uid: str = Depends
|
|
| 127 |
|
| 128 |
gemini = GeminiService()
|
| 129 |
|
|
|
|
| 130 |
async def generate():
|
| 131 |
full_response = ""
|
| 132 |
try:
|
| 133 |
async for chunk in gemini.stream(payload.query, context=context, chat_history=chat_history):
|
| 134 |
full_response += chunk
|
| 135 |
# Format as SSE
|
| 136 |
-
yield f"data: {chunk}\n\n"
|
| 137 |
|
| 138 |
# Save full response to memory after streaming completes
|
| 139 |
engine.memory.add_message("assistant", full_response)
|
| 140 |
yield "data: [DONE]\n\n"
|
| 141 |
except Exception as e:
|
| 142 |
logger.error(f"Streaming error: {e}")
|
| 143 |
-
yield f"data: ⚠️ Streaming error: {e}\n\n"
|
| 144 |
yield "data: [DONE]\n\n"
|
| 145 |
|
| 146 |
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
|
|
| 127 |
|
| 128 |
gemini = GeminiService()
|
| 129 |
|
| 130 |
+
import json
|
| 131 |
async def generate():
|
| 132 |
full_response = ""
|
| 133 |
try:
|
| 134 |
async for chunk in gemini.stream(payload.query, context=context, chat_history=chat_history):
|
| 135 |
full_response += chunk
|
| 136 |
# Format as SSE
|
| 137 |
+
yield f"data: {json.dumps({'content': chunk, 'type': 'token'})}\n\n"
|
| 138 |
|
| 139 |
# Save full response to memory after streaming completes
|
| 140 |
engine.memory.add_message("assistant", full_response)
|
| 141 |
yield "data: [DONE]\n\n"
|
| 142 |
except Exception as e:
|
| 143 |
logger.error(f"Streaming error: {e}")
|
| 144 |
+
yield f"data: {json.dumps({'content': f'\\n\\n⚠️ Streaming error: {e}', 'type': 'token'})}\n\n"
|
| 145 |
yield "data: [DONE]\n\n"
|
| 146 |
|
| 147 |
return StreamingResponse(generate(), media_type="text/event-stream")
|
src/services/gemini_service.py
CHANGED
|
@@ -163,9 +163,32 @@ class GeminiService:
|
|
| 163 |
if chunk.text:
|
| 164 |
yield chunk.text
|
| 165 |
except Exception as e:
|
| 166 |
-
logger.error(f"
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
class GeminiVisionService:
|
| 171 |
"""Gemini Vision for extracting math from images."""
|
|
|
|
| 163 |
if chunk.text:
|
| 164 |
yield chunk.text
|
| 165 |
except Exception as e:
|
| 166 |
+
logger.error(f"Primary model {self.primary_model} streaming failed: {e}")
|
| 167 |
+
logger.info(f"Falling back to {self.fallback_model}...")
|
| 168 |
+
try:
|
| 169 |
+
response_stream = client.models.generate_content_stream(
|
| 170 |
+
model=self.fallback_model,
|
| 171 |
+
contents=prompt,
|
| 172 |
+
config=config
|
| 173 |
+
)
|
| 174 |
+
for chunk in response_stream:
|
| 175 |
+
if chunk.text:
|
| 176 |
+
yield chunk.text
|
| 177 |
+
except Exception as e2:
|
| 178 |
+
logger.error(f"Fallback model {self.fallback_model} streaming also failed: {e2}")
|
| 179 |
+
logger.info("Falling back to gemini-1.5-flash-8b...")
|
| 180 |
+
try:
|
| 181 |
+
response_stream = client.models.generate_content_stream(
|
| 182 |
+
model="gemini-1.5-flash-8b",
|
| 183 |
+
contents=prompt,
|
| 184 |
+
config=config
|
| 185 |
+
)
|
| 186 |
+
for chunk in response_stream:
|
| 187 |
+
if chunk.text:
|
| 188 |
+
yield chunk.text
|
| 189 |
+
except Exception as e3:
|
| 190 |
+
logger.error(f"All Gemini streaming models failed: {e3}")
|
| 191 |
+
yield f"\n\n⚠️ Streaming error: All Gemini models failed. You might be out of API quota."
|
| 192 |
|
| 193 |
class GeminiVisionService:
|
| 194 |
"""Gemini Vision for extracting math from images."""
|