Spaces:
Sleeping
Stream LLM Responses (#18)
Browse files* feat: add SSE streaming endpoint for faster response display
Add /chat-stream SSE endpoint that sends advisor responses incrementally as they complete. Rewrite ChatPage handleSendMessage to consume SSE events. Add synthesize_responses method to orchestrator for aggregate mode support.
Made-with: Cursor
* Clean up changes to reduce diff and clean up unused imports
* Replace removed comments for code clarity
* Remove vestigial synthesized response handling
* Remove invalid kwarg
* Implement Cursor-generated fix to parallel response generation
* Undo `json` import name override
* Clean up unused params
* Refactor repeated message handling to match existing implementation
* Refactor to return JSON objects instead of specifically-formatted plaintext
* Remove trailing newline to reduce PR diff
* Update `chat_sequential_enhanced` docstring to be consistent with streaming response docstring
* Address review comments
* Remove unused `active_advisors` handling to simplify advisor selection
---------
Co-authored-by: NeonClary <askclary@gmail.com>
|
@@ -1,15 +1,20 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from app.api.utils import get_or_create_session_for_request_async
|
| 5 |
-
from app.core.bootstrap import chat_orchestrator
|
| 6 |
from app.core.auth import get_current_active_user
|
| 7 |
-
from app.
|
| 8 |
-
from pydantic import BaseModel
|
| 9 |
-
from typing import Optional
|
| 10 |
-
import logging
|
| 11 |
from app.core.database import get_database
|
| 12 |
-
from
|
|
|
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
| 15 |
|
|
@@ -25,6 +30,7 @@ class ChatMessage(BaseModel):
|
|
| 25 |
session_id: Optional[str] = None
|
| 26 |
chat_session_id: Optional[str] = None # MongoDB chat session ID
|
| 27 |
response_length: str = "medium"
|
|
|
|
| 28 |
|
| 29 |
class ReplyToAdvisor(BaseModel):
|
| 30 |
user_input: str
|
|
@@ -42,6 +48,140 @@ class SwitchChatRequest(BaseModel):
|
|
| 42 |
class NewChatRequest(BaseModel):
|
| 43 |
title: Optional[str] = "New Chat"
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
@router.post("/switch-chat")
|
| 46 |
async def switch_to_chat(
|
| 47 |
request: SwitchChatRequest,
|
|
@@ -157,9 +297,13 @@ async def chat_sequential_enhanced(
|
|
| 157 |
message: ChatMessage,
|
| 158 |
request: Request,
|
| 159 |
current_user: User = Depends(get_current_active_user)
|
| 160 |
-
):
|
| 161 |
"""
|
| 162 |
Enhanced sequential chat with proper session management, document access, and intelligent persona ordering
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
"""
|
| 164 |
try:
|
| 165 |
# Ensure consistent session ID for document retrieval
|
|
@@ -205,7 +349,7 @@ async def chat_sequential_enhanced(
|
|
| 205 |
session.append_message("user", message.user_input)
|
| 206 |
|
| 207 |
# Check if the user's message is vague and needs clarification
|
| 208 |
-
if chat_orchestrator.
|
| 209 |
clarification = await chat_orchestrator.generate_contextual_clarification(
|
| 210 |
message.user_input
|
| 211 |
)
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import traceback
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
|
| 7 |
+
from bson import ObjectId
|
| 8 |
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 9 |
+
from fastapi.responses import StreamingResponse
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
|
| 12 |
from app.api.utils import get_or_create_session_for_request_async
|
|
|
|
| 13 |
from app.core.auth import get_current_active_user
|
| 14 |
+
from app.core.bootstrap import chat_orchestrator
|
|
|
|
|
|
|
|
|
|
| 15 |
from app.core.database import get_database
|
| 16 |
+
from app.core.session_manager import get_session_manager
|
| 17 |
+
from app.models.user import User
|
| 18 |
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
|
|
|
| 30 |
session_id: Optional[str] = None
|
| 31 |
chat_session_id: Optional[str] = None # MongoDB chat session ID
|
| 32 |
response_length: str = "medium"
|
| 33 |
+
active_advisors: Optional[List[str]] = None
|
| 34 |
|
| 35 |
class ReplyToAdvisor(BaseModel):
|
| 36 |
user_input: str
|
|
|
|
| 48 |
class NewChatRequest(BaseModel):
|
| 49 |
title: Optional[str] = "New Chat"
|
| 50 |
|
| 51 |
+
ChatStreamEventType = Literal["error", "progress", "clarification", "advisor"]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ChatStreamLine(BaseModel):
|
| 55 |
+
"""One NDJSON line from ``/chat-stream``."""
|
| 56 |
+
|
| 57 |
+
type: ChatStreamEventType
|
| 58 |
+
data: Dict[str, Any] = Field(default_factory=dict)
|
| 59 |
+
|
| 60 |
+
def to_ndjson(self) -> str:
|
| 61 |
+
return json.dumps(self.model_dump(mode="json"), ensure_ascii=False) + "\n"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@router.post("/chat-stream")
|
| 65 |
+
async def chat_stream(
|
| 66 |
+
message: ChatMessage,
|
| 67 |
+
request: Request,
|
| 68 |
+
current_user: User = Depends(get_current_active_user),
|
| 69 |
+
) -> StreamingResponse:
|
| 70 |
+
"""
|
| 71 |
+
Streaming variant of chat-sequential (newline-delimited JSON).
|
| 72 |
+
@param message: ChatMessage containing user input and optional session/chat IDs
|
| 73 |
+
@param request: FastAPI Request object for session management
|
| 74 |
+
@param current_user: Authenticated user from dependency injection
|
| 75 |
+
@return: StreamingResponse that yields ChatStreamLine events as NDJSON
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
async def _event_generator():
|
| 79 |
+
try:
|
| 80 |
+
if message.chat_session_id:
|
| 81 |
+
sid = f"chat_{message.chat_session_id}"
|
| 82 |
+
if sid not in session_manager.sessions:
|
| 83 |
+
sid = await get_or_create_session_for_request_async(
|
| 84 |
+
request,
|
| 85 |
+
chat_session_id=message.chat_session_id,
|
| 86 |
+
user_id=str(current_user.id),
|
| 87 |
+
)
|
| 88 |
+
else:
|
| 89 |
+
sid = await get_or_create_session_for_request_async(request)
|
| 90 |
+
|
| 91 |
+
session = session_manager.get_session(sid)
|
| 92 |
+
|
| 93 |
+
if (
|
| 94 |
+
len(session.messages) > 0 and
|
| 95 |
+
session.messages[-1].get("role") == "user" and
|
| 96 |
+
session.messages[-1].get("content") == message.user_input
|
| 97 |
+
):
|
| 98 |
+
# TODO: This should be handled in the front-end input
|
| 99 |
+
logger.warning(f"Repeated user input: {message.user_input}")
|
| 100 |
+
|
| 101 |
+
session.append_message("user", message.user_input)
|
| 102 |
+
|
| 103 |
+
if chat_orchestrator.needs_clarification(session, message.user_input):
|
| 104 |
+
clar = await chat_orchestrator.generate_contextual_clarification(message.user_input)
|
| 105 |
+
yield ChatStreamLine(
|
| 106 |
+
type="clarification",
|
| 107 |
+
data={
|
| 108 |
+
"message": clar["question"],
|
| 109 |
+
"suggestions": clar["suggestions"],
|
| 110 |
+
},
|
| 111 |
+
).to_ndjson()
|
| 112 |
+
yield ChatStreamLine(
|
| 113 |
+
type="progress",
|
| 114 |
+
data={"phase": "complete"},
|
| 115 |
+
).to_ndjson()
|
| 116 |
+
return
|
| 117 |
+
|
| 118 |
+
# Get personas most relevant to the current session
|
| 119 |
+
top_personas = await chat_orchestrator.get_top_personas(
|
| 120 |
+
session_id=sid,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
done_queue: asyncio.Queue = asyncio.Queue()
|
| 124 |
+
|
| 125 |
+
async def _run(pid: str) -> None:
|
| 126 |
+
try:
|
| 127 |
+
persona = chat_orchestrator.get_persona(pid)
|
| 128 |
+
result = await chat_orchestrator.generate_single_persona_response(
|
| 129 |
+
session, persona,
|
| 130 |
+
message.response_length or "medium",
|
| 131 |
+
)
|
| 132 |
+
session.append_message(pid, result["response"])
|
| 133 |
+
await done_queue.put(result)
|
| 134 |
+
except Exception as e:
|
| 135 |
+
logger.exception(f"chat-stream _run failed for {pid}: {e}")
|
| 136 |
+
await done_queue.put({
|
| 137 |
+
"persona_id": persona.id,
|
| 138 |
+
"persona_name": persona.name,
|
| 139 |
+
"response": f"I ran into a technical issue. Please try again. ({e!s})",
|
| 140 |
+
"used_documents": False,
|
| 141 |
+
"document_chunks_used": 0,
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
+
tasks = [asyncio.create_task(_run(pid)) for pid in top_personas]
|
| 145 |
+
|
| 146 |
+
for _ in range(len(tasks)):
|
| 147 |
+
result = await done_queue.get()
|
| 148 |
+
line = ChatStreamLine(
|
| 149 |
+
type="advisor",
|
| 150 |
+
data={
|
| 151 |
+
"persona_id": result["persona_id"],
|
| 152 |
+
"persona_name": result["persona_name"],
|
| 153 |
+
"content": result["response"],
|
| 154 |
+
"used_documents": result.get("used_documents", False),
|
| 155 |
+
"document_chunks_used": result.get("document_chunks_used", 0),
|
| 156 |
+
},
|
| 157 |
+
)
|
| 158 |
+
yield line.to_ndjson()
|
| 159 |
+
|
| 160 |
+
await asyncio.gather(*tasks, return_exceptions=True)
|
| 161 |
+
|
| 162 |
+
yield ChatStreamLine(
|
| 163 |
+
type="progress",
|
| 164 |
+
data={"phase": "complete"},
|
| 165 |
+
).to_ndjson()
|
| 166 |
+
|
| 167 |
+
except Exception as exc:
|
| 168 |
+
logger.error(f"chat-stream error: {exc}")
|
| 169 |
+
logger.error(traceback.format_exc())
|
| 170 |
+
yield ChatStreamLine(
|
| 171 |
+
type="error",
|
| 172 |
+
data={"detail": str(exc)},
|
| 173 |
+
).to_ndjson()
|
| 174 |
+
|
| 175 |
+
return StreamingResponse(
|
| 176 |
+
_event_generator(),
|
| 177 |
+
media_type="application/x-ndjson",
|
| 178 |
+
headers={
|
| 179 |
+
"Cache-Control": "no-cache",
|
| 180 |
+
"X-Accel-Buffering": "no",
|
| 181 |
+
},
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
@router.post("/switch-chat")
|
| 186 |
async def switch_to_chat(
|
| 187 |
request: SwitchChatRequest,
|
|
|
|
| 297 |
message: ChatMessage,
|
| 298 |
request: Request,
|
| 299 |
current_user: User = Depends(get_current_active_user)
|
| 300 |
+
) -> Dict[str, Any]:
|
| 301 |
"""
|
| 302 |
Enhanced sequential chat with proper session management, document access, and intelligent persona ordering
|
| 303 |
+
@param message: ChatMessage containing user input and optional session/chat IDs
|
| 304 |
+
@param request: FastAPI Request object for session management
|
| 305 |
+
@param current_user: Authenticated user from dependency injection
|
| 306 |
+
@return: Dict response with LLM responses if successful, else error details
|
| 307 |
"""
|
| 308 |
try:
|
| 309 |
# Ensure consistent session ID for document retrieval
|
|
|
|
| 349 |
session.append_message("user", message.user_input)
|
| 350 |
|
| 351 |
# Check if the user's message is vague and needs clarification
|
| 352 |
+
if chat_orchestrator.needs_clarification(session, message.user_input):
|
| 353 |
clarification = await chat_orchestrator.generate_contextual_clarification(
|
| 354 |
message.user_input
|
| 355 |
)
|
|
@@ -49,7 +49,7 @@ class ImprovedChatOrchestrator:
|
|
| 49 |
session.append_message("user", user_input)
|
| 50 |
|
| 51 |
# Determine if we need clarification
|
| 52 |
-
needs_clarification = self.
|
| 53 |
|
| 54 |
if needs_clarification:
|
| 55 |
# Generate clarification question
|
|
@@ -64,7 +64,7 @@ class ImprovedChatOrchestrator:
|
|
| 64 |
}
|
| 65 |
|
| 66 |
# Generate responses from all personas
|
| 67 |
-
responses = await self.
|
| 68 |
|
| 69 |
return {
|
| 70 |
"status": "success",
|
|
@@ -100,7 +100,7 @@ class ImprovedChatOrchestrator:
|
|
| 100 |
available_documents = [doc["filename"] for doc in doc_stats.get("documents", [])]
|
| 101 |
|
| 102 |
# Generate enhanced persona responses
|
| 103 |
-
responses = await self.
|
| 104 |
|
| 105 |
return {
|
| 106 |
"status": "success",
|
|
@@ -136,7 +136,7 @@ class ImprovedChatOrchestrator:
|
|
| 136 |
|
| 137 |
return references[:3] # Limit to first 3 references
|
| 138 |
|
| 139 |
-
def
|
| 140 |
"""
|
| 141 |
Determine if the user input needs clarification.
|
| 142 |
Patterns and keywords are driven by config.yaml → orchestrator section.
|
|
@@ -253,7 +253,7 @@ class ImprovedChatOrchestrator:
|
|
| 253 |
"suggestions": fallback_suggestions,
|
| 254 |
}
|
| 255 |
|
| 256 |
-
async def
|
| 257 |
"""
|
| 258 |
Generate responses from all personas with enhanced RAG integration
|
| 259 |
"""
|
|
@@ -263,7 +263,7 @@ class ImprovedChatOrchestrator:
|
|
| 263 |
logger.info(f"Generating response for {persona_id} with enhanced RAG")
|
| 264 |
|
| 265 |
# Generate persona response with enhanced RAG
|
| 266 |
-
response_data = await self.
|
| 267 |
|
| 268 |
# Add persona response to session context
|
| 269 |
session.append_message(persona_id, response_data["response"])
|
|
@@ -272,7 +272,7 @@ class ImprovedChatOrchestrator:
|
|
| 272 |
|
| 273 |
return responses
|
| 274 |
|
| 275 |
-
async def
|
| 276 |
"""
|
| 277 |
Enhanced version - Generate response from a single persona with enhanced RAG integration
|
| 278 |
"""
|
|
@@ -700,7 +700,7 @@ When analyzing the document context:
|
|
| 700 |
logger.info(f"Generating response for {persona_id} with session {session_id}")
|
| 701 |
|
| 702 |
# Generate response from single persona using consistent session ID
|
| 703 |
-
response_data = await self.
|
| 704 |
|
| 705 |
# Add response to session
|
| 706 |
session.append_message(persona_id, response_data["response"])
|
|
@@ -771,6 +771,9 @@ When analyzing the document context:
|
|
| 771 |
for p in self.personas.values()
|
| 772 |
])
|
| 773 |
|
|
|
|
|
|
|
|
|
|
| 774 |
app_title = get_settings().app.title
|
| 775 |
|
| 776 |
prompt = f"""
|
|
|
|
| 49 |
session.append_message("user", user_input)
|
| 50 |
|
| 51 |
# Determine if we need clarification
|
| 52 |
+
needs_clarification = self.needs_clarification(session, user_input)
|
| 53 |
|
| 54 |
if needs_clarification:
|
| 55 |
# Generate clarification question
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
# Generate responses from all personas
|
| 67 |
+
responses = await self.generate_persona_responses(session, response_length)
|
| 68 |
|
| 69 |
return {
|
| 70 |
"status": "success",
|
|
|
|
| 100 |
available_documents = [doc["filename"] for doc in doc_stats.get("documents", [])]
|
| 101 |
|
| 102 |
# Generate enhanced persona responses
|
| 103 |
+
responses = await self.generate_persona_responses(session, response_length)
|
| 104 |
|
| 105 |
return {
|
| 106 |
"status": "success",
|
|
|
|
| 136 |
|
| 137 |
return references[:3] # Limit to first 3 references
|
| 138 |
|
| 139 |
+
def needs_clarification(self, session: ConversationContext, user_input: str) -> bool:
|
| 140 |
"""
|
| 141 |
Determine if the user input needs clarification.
|
| 142 |
Patterns and keywords are driven by config.yaml → orchestrator section.
|
|
|
|
| 253 |
"suggestions": fallback_suggestions,
|
| 254 |
}
|
| 255 |
|
| 256 |
+
async def generate_persona_responses(self, session: ConversationContext, response_length: str = "medium"):
|
| 257 |
"""
|
| 258 |
Generate responses from all personas with enhanced RAG integration
|
| 259 |
"""
|
|
|
|
| 263 |
logger.info(f"Generating response for {persona_id} with enhanced RAG")
|
| 264 |
|
| 265 |
# Generate persona response with enhanced RAG
|
| 266 |
+
response_data = await self.generate_single_persona_response(session, persona, response_length)
|
| 267 |
|
| 268 |
# Add persona response to session context
|
| 269 |
session.append_message(persona_id, response_data["response"])
|
|
|
|
| 272 |
|
| 273 |
return responses
|
| 274 |
|
| 275 |
+
async def generate_single_persona_response(self, session, persona, response_length: str = "medium"):
|
| 276 |
"""
|
| 277 |
Enhanced version - Generate response from a single persona with enhanced RAG integration
|
| 278 |
"""
|
|
|
|
| 700 |
logger.info(f"Generating response for {persona_id} with session {session_id}")
|
| 701 |
|
| 702 |
# Generate response from single persona using consistent session ID
|
| 703 |
+
response_data = await self.generate_single_persona_response(session, persona, response_length)
|
| 704 |
|
| 705 |
# Add response to session
|
| 706 |
session.append_message(persona_id, response_data["response"])
|
|
|
|
| 771 |
for p in self.personas.values()
|
| 772 |
])
|
| 773 |
|
| 774 |
+
# Ensure k does not exceed the number of available personas
|
| 775 |
+
k = min(k, len(self.personas))
|
| 776 |
+
|
| 777 |
app_title = get_settings().app.title
|
| 778 |
|
| 779 |
prompt = f"""
|
|
@@ -396,17 +396,12 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 396 |
setThinkingAdvisors(['system']);
|
| 397 |
|
| 398 |
try {
|
| 399 |
-
const
|
| 400 |
-
'Content-Type': 'application/json',
|
| 401 |
-
'Authorization': `Bearer ${authToken}`,
|
| 402 |
-
};
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
console.log('Sending message with session ID:', currentSessionId); // Debug log
|
| 406 |
-
|
| 407 |
-
const response = await fetch(`${process.env.REACT_APP_API_URL}/chat-sequential`, {
|
| 408 |
method: 'POST',
|
| 409 |
-
headers:
|
|
|
|
|
|
|
|
|
|
| 410 |
body: JSON.stringify({
|
| 411 |
user_input: inputMessage,
|
| 412 |
response_length: 'medium',
|
|
@@ -418,32 +413,69 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 418 |
throw new Error(`HTTP error! status: ${response.status}`);
|
| 419 |
}
|
| 420 |
|
| 421 |
-
|
| 422 |
-
const
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
const
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
}
|
| 448 |
}
|
| 449 |
|
|
|
|
| 396 |
setThinkingAdvisors(['system']);
|
| 397 |
|
| 398 |
try {
|
| 399 |
+
const response = await fetch(`${process.env.REACT_APP_API_URL}/chat-stream`, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
method: 'POST',
|
| 401 |
+
headers: {
|
| 402 |
+
'Content-Type': 'application/json',
|
| 403 |
+
'Authorization': `Bearer ${authToken}`,
|
| 404 |
+
},
|
| 405 |
body: JSON.stringify({
|
| 406 |
user_input: inputMessage,
|
| 407 |
response_length: 'medium',
|
|
|
|
| 413 |
throw new Error(`HTTP error! status: ${response.status}`);
|
| 414 |
}
|
| 415 |
|
| 416 |
+
const reader = response.body.getReader();
|
| 417 |
+
const decoder = new TextDecoder();
|
| 418 |
+
let buffer = '';
|
| 419 |
+
|
| 420 |
+
while (true) {
|
| 421 |
+
const { done, value } = await reader.read();
|
| 422 |
+
if (done) break;
|
| 423 |
+
|
| 424 |
+
buffer += decoder.decode(value, { stream: true });
|
| 425 |
+
const lines = buffer.split('\n');
|
| 426 |
+
buffer = lines.pop() ?? '';
|
| 427 |
+
|
| 428 |
+
for (const line of lines) {
|
| 429 |
+
if (!line.trim()) continue;
|
| 430 |
+
const payload = JSON.parse(line);
|
| 431 |
+
|
| 432 |
+
const d = payload.data || {};
|
| 433 |
+
|
| 434 |
+
switch (payload.type) {
|
| 435 |
+
case 'advisor': {
|
| 436 |
+
const msg = {
|
| 437 |
+
id: generateMessageId(),
|
| 438 |
+
type: 'advisor',
|
| 439 |
+
persona_id: d.persona_id,
|
| 440 |
+
content: d.content,
|
| 441 |
+
timestamp: new Date(),
|
| 442 |
+
advisorName: d.persona_name || d.persona_id,
|
| 443 |
+
used_documents: d.used_documents || false,
|
| 444 |
+
document_chunks_used: d.document_chunks_used || 0,
|
| 445 |
+
};
|
| 446 |
+
setMessages(prev => [...prev, msg]);
|
| 447 |
+
setThinkingAdvisors(prev => prev.filter(a => a !== d.persona_id));
|
| 448 |
+
await saveMessageToSession(msg);
|
| 449 |
+
break;
|
| 450 |
+
}
|
| 451 |
+
case 'clarification':
|
| 452 |
+
setMessages(prev => [...prev, {
|
| 453 |
+
id: generateMessageId(),
|
| 454 |
+
type: 'clarification',
|
| 455 |
+
content: d.message,
|
| 456 |
+
suggestions: d.suggestions || [],
|
| 457 |
+
timestamp: new Date(),
|
| 458 |
+
}]);
|
| 459 |
+
break;
|
| 460 |
+
case 'progress':
|
| 461 |
+
if (d.phase === 'complete') {
|
| 462 |
+
break;
|
| 463 |
+
}
|
| 464 |
+
if (d.persona_id != null) {
|
| 465 |
+
setThinkingAdvisors(prev => prev.filter(a => a !== d.persona_id));
|
| 466 |
+
}
|
| 467 |
+
break;
|
| 468 |
+
case 'error':
|
| 469 |
+
setMessages(prev => [...prev, {
|
| 470 |
+
id: generateMessageId(),
|
| 471 |
+
type: 'error',
|
| 472 |
+
content: d.detail || 'An error occurred',
|
| 473 |
+
timestamp: new Date(),
|
| 474 |
+
}]);
|
| 475 |
+
break;
|
| 476 |
+
default:
|
| 477 |
+
break;
|
| 478 |
+
}
|
| 479 |
}
|
| 480 |
}
|
| 481 |
|