feat(ui): explicit chat state machine — typing indicator, streaming progress, timeout and error recovery
This commit is contained in:
+30
-13
@@ -34,7 +34,7 @@ from app.rag.llm import EmbeddingError, LLMClient, LLMError
|
||||
from app.rag.prompts import build_deflect_prompt, build_high_prompt
|
||||
from app.rag.retriever import RetrievedChunk, retrieve, select_documents, weak_hit_titles
|
||||
from app.rag.suggestions import derive_suggestions
|
||||
from app.schemas import ChatDoneEvent, ChatRequest, SourceRef
|
||||
from app.schemas import ChatDoneEvent, ChatErrorEvent, ChatRequest, SourceRef
|
||||
|
||||
logger = logging.getLogger("app.chat")
|
||||
router = APIRouter(tags=["chat"])
|
||||
@@ -122,12 +122,19 @@ async def chat(
|
||||
try:
|
||||
question_vec = await llm.embed_one(request.message)
|
||||
except EmbeddingError as e:
|
||||
logger.error("chat: embedding failed question=%r — %s", request.message, e)
|
||||
embed_ms = int((time.monotonic() - t0) * 1000)
|
||||
total_ms = int((time.monotonic() - started) * 1000)
|
||||
logger.error(
|
||||
"chat: question=%r embed_ms=%d total_ms=%d — embedding failed: %s",
|
||||
request.message,
|
||||
embed_ms,
|
||||
total_ms,
|
||||
e,
|
||||
)
|
||||
yield sse_event(
|
||||
{
|
||||
"type": "error",
|
||||
"detail": "I couldn't reach the embedding model — please try again.",
|
||||
}
|
||||
ChatErrorEvent(
|
||||
detail="I couldn't reach the embedding model — please try again."
|
||||
).model_dump()
|
||||
)
|
||||
return
|
||||
embed_ms = int((time.monotonic() - t0) * 1000)
|
||||
@@ -139,12 +146,15 @@ async def chat(
|
||||
chunks = retrieve(db, question_vec)
|
||||
plan = plan_turn(chunks, settings)
|
||||
except Exception: # noqa: BLE001 — DB failure mid-turn
|
||||
logger.exception("chat: retrieval failed question=%r", request.message)
|
||||
logger.exception(
|
||||
"chat: retrieval failed question=%r total_ms=%d",
|
||||
request.message,
|
||||
int((time.monotonic() - started) * 1000),
|
||||
)
|
||||
yield sse_event(
|
||||
{
|
||||
"type": "error",
|
||||
"detail": "The knowledge base went offline mid-question — is Postgres up?",
|
||||
}
|
||||
ChatErrorEvent(
|
||||
detail="The knowledge base went offline mid-question — is Postgres up?"
|
||||
).model_dump()
|
||||
)
|
||||
return
|
||||
source_paths = [f"{d.source}/{d.path}" for d in plan.docs]
|
||||
@@ -158,9 +168,16 @@ async def chat(
|
||||
async for piece in llm.chat_stream(messages):
|
||||
yield sse_event({"type": "delta", "text": piece})
|
||||
except LLMError as e:
|
||||
logger.error("chat: LLM stream failed question=%r — %s", request.message, e)
|
||||
logger.error(
|
||||
"chat: LLM stream failed question=%r total_ms=%d — %s",
|
||||
request.message,
|
||||
int((time.monotonic() - started) * 1000),
|
||||
e,
|
||||
)
|
||||
yield sse_event(
|
||||
{"type": "error", "detail": "The chat model dropped the connection — try again?"}
|
||||
ChatErrorEvent(
|
||||
detail="The chat model dropped the connection — try again?"
|
||||
).model_dump()
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user