phase: 114_embed_question_length
Build and Push Containers / build-and-push-app (push) Successful in 2m6s
Build and Push Containers / build-and-push-db (push) Successful in 13s

All verification passes complete — the phase was already fully implemented in the working tree, and every gate is green. No defects found; no code changes were needed.

**Final verification pass — Phase 114 (embed question length):**
- Verified truncation: `chat.py:459` embeds `request.message[:settings.embed_question_max_chars]` (default 1200, `BOR_EMBED_QUESTION_MAX_CHARS`, `>0` validator); full question still reaches the LLM prompt/log.
- Verified error mapping: `EmbeddingInputTooLargeError(EmbeddingError)` (byte-identical message) caught **before** `EmbeddingError` → no retry, terminal frame `detail="Question too long — trim it and re-ask."` + reachability-fine hint; `ChatErrorEvent.hint` additive.
- Verified frontend chain: frame `hint` → `err.hint` → `setUiState(error, …, {hint})` → `showErrorBanner(…, {retryable: true})` — hint replaces default `ERROR_HINT`, phase-111 `#banner-retry` button revealed. 4,000-char clamp untouched (A2).
- `uv run pytest tests/unit/test_embed_question_length.py -v --no-cov` → 21 passed
- `uv run pytest tests/e2e/test_embed_question_length.py -v --no-cov` (isolation, DB up) → 1 passed (4,000-char question → done, no banner)
- Regression: `test_llm_retry.py` 4 passed · `test_oneshot_llm_retry.py` 2 passed · `test_chip_sizing_question_cap.py` 6 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 2444 passed, TOTAL **99%** (>90% gate)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings

**Completion criteria:** (1) 4,000-char question embeds prefix + full prompt ✅ · (2) too-large → accurate frame + hint + Retry button ✅ · (3) reachability failure byte-identical (retries + old copy) ✅ · (4) all gates green ✅ · (5) commit/phase-move → left to the harness per instructions (no `git add`/`commit` run).
**Deviations:** none. **Next pending phase:** `115_doc_draft_discard`.
This commit is contained in:
2026-09-15 04:16:55 -04:00
parent 97d663d16d
commit 3846f26a58
26 changed files with 1397 additions and 20 deletions
+38 -1
View File
@@ -184,6 +184,7 @@ from app.rag.agent import (
)
from app.rag.llm import (
EmbeddingError,
EmbeddingInputTooLargeError, # phase 114: the deterministic too-large failure
LLMClient,
LLMError,
RetryPiece, # phase 67: one LLM request restart (an SSE retry frame)
@@ -450,8 +451,44 @@ async def chat(
attempt = 1
while True:
try:
question_vec = await llm.embed_one(request.message)
# Phase 114 (TODO L6): the embed input is bounded to the
# model's per-request input cap (the chunker's 1200-char
# budget, env-tunable) — the FULL question still reaches
# the LLM prompt (prompt build + log line untouched).
question_vec = await llm.embed_one(
request.message[: settings.embed_question_max_chars]
)
break
except EmbeddingInputTooLargeError as e:
# Phase 114 (TODO L6, locked A3): a too-large input is
# DETERMINISTIC — retrying the same size is guaranteed
# to repeat — so this short-circuits the phase-67 retry
# loop: no ``retry`` frame, no restart, one terminal
# error frame with the accurate "question too long"
# detail + the reachability-fine hint (the
# "couldn't reach" copy and the retry budget stay for
# reachability failures only — the branch below).
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 (too-large): %s",
request.message,
embed_ms,
total_ms,
e,
)
settled = True # terminal: the error frame settles the turn
yield sse_event(
ChatErrorEvent(
detail="Question too long — trim it and re-ask.",
hint=(
"The app reached the embedding model fine — "
"only the question length is the problem."
),
).model_dump()
)
return
except EmbeddingError as e:
embed_ms = int((time.monotonic() - t0) * 1000)
if attempt >= max_attempts: