feat(rag): pass chat history with prior thinking to the LLM
Phase 74 (TODO.md L4): a follow-up question now reaches the model WITH the conversation so far — every prior user/brain turn and the prior thinking blocks on brain turns (preserve-thinking) — while POST /api/chat stays stateless (A10): the client provides the history in the request body and the server stores nothing new. Server (task 01): - ChatRequest.history: optional list[HistoryTurn] (who: user|brain, text, optional thinking) — absent/empty keeps the request byte-identical to pre-phase-74 (the two-message [system, user] request; the kill-switch semantics are pinned in the integration suite). - app.rag.prompts.history_to_messages: pure mapper — walks the turns newest-first against the settings budgets (history_max_turns=40 / history_max_chars=24000, BOR_HISTORY_MAX_TURNS / BOR_HISTORY_MAX_CHARS); a capped turn is dropped WHOLE (never cut mid-answer); the kept window is returned oldest-first; brain turns carry their thinking as reasoning_content (A4) only when non-empty. - Both branches feed it: the deflected path splices it between the system prompt and the current user message (the phase-71 recovery still rebuilds from messages[1:]), the grounded agent receives run_agent(..., history=hist); llm.py's message params widen to list[dict[str, Any]] (string-only messages stay byte-identical on the wire — the SDK passes message dicts through verbatim). - The per-turn log line (PLAN §9) gains history_msgs=N after kb_chars=N. - Pins: tests/unit/test_history.py (mapper: mapping, reasoning gating, both budgets, drop-whole, ordering, empty default), tests/unit/test_config.py (the two settings + env overrides), tests/unit/test_agent.py (the history splice + the default), tests/integration/test_chat_api.py (deflected AND grounded forward the history incl. reasoning_content, no-history byte-identity, 422 pins, the log field). Client (task 02): - runTurn — the single funnel for fresh send / phase-49 retry / phase-53 stale-regen — sends history = the conversation record minus the current question, with thinking only on brain records that streamed one (undefined drops the key from the JSON, the record's convention); the question is never duplicated into the history. Wire proof (task 03): - The mock's echo my history marker (HISTORY_TRIGGER) answers with the deterministic history echo — history: N prior messages; last answer tail: <last 24 chars>; thinking: yes|no — checked BEFORE the DEFLECT_MODE branch (like TABLE_TRIGGER), so it fires on both turn branches whatever the gate says; the module docstring records the user/assistant-only history invariant that keeps every existing (tool-result-classified) marker flow unaffected. - tests/e2e/test_llm_history.py (isolated): a grounded follow-up and a deflected follow-up both receive history: 2 prior messages + thinking: yes + the byte-exact tail of turn 1's answer (derived from the persisted bor.chat.v1 record — the same array the client maps into the body); a cold start receives history: 0 prior messages / last answer tail: none / thinking: no. - Regressions green in isolation: chat_rag, chat_history (phase 50), agent_document_tools, harness_aligned_tools, stop_generation, retry_answer, response_to_docs.
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
All done — full gate green. Final report:
|
||||
|
||||
**Implemented (phase 74, task 01 — server-side history ingestion):**
|
||||
- `app/config.py`: `history_max_turns=40` / `history_max_chars=24000` (`BOR_`-overridable, fail-loud validators on negatives, house pattern)
|
||||
- `app/schemas.py`: `HistoryTurn` + `ChatRequest.history` (default `[]`, schema ceiling 100)
|
||||
- `app/rag/prompts.py`: pure `history_to_messages()` — newest-first walk, both budgets, drop-whole (contiguous newest window), chronological output, `reasoning_content` only when thinking non-empty
|
||||
- `app/api/chat.py`: `hist` mapped once per turn; deflected branch splices it into `messages`; grounded passes `history=hist` to `run_agent`; log line gains `history_msgs=N` after `kb_chars=N`
|
||||
- `app/rag/agent.py`: `run_agent(..., history=())` spliced between system and current user; obsolete `cast`s dropped; tool rounds / recovery / retries unchanged
|
||||
- `app/rag/llm.py`: `chat`/`chat_stream`/`chat_stream_retried` widened to `list[dict[str, Any]]` (no transport change)
|
||||
- Tests: 16 new in `tests/unit/test_history.py`, 4 in `test_config.py`, 3 in `test_agent.py`, 5 in `tests/integration/test_chat_api.py` (deflected + grounded forwarding incl. `reasoning_content`, no-history byte-identical pin, 422s, caplog `history_msgs`)
|
||||
|
||||
**Results:** `uv run pytest` → 1547 passed (baseline 1519); `uv run pytest --cov=app` → TOTAL 99% (>90%, all touched files 100%); `uv run ruff check .` → clean; `uv run pyright` → 0 errors; `uv run .agents/validate.sh` → exit 0.
|
||||
|
||||
**Notes:** `.agents/PLAN.md` is absent from the repo — worked from the phase overview + task spec (A2/A3/A4 assumptions). Trim semantics implemented as contiguous newest window (walk stops at first non-fitting turn). No commit, per task (task 03 commits).
|
||||
|
||||
**Next pending task:** `.agents/phases/todo/74_llm_chat_history/02_client_sends_history.md`
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 9%]
|
||||
........................................................................ [ 13%]
|
||||
........................................................................ [ 18%]
|
||||
........................................................................ [ 23%]
|
||||
........................................................................ [ 27%]
|
||||
........................................................................ [ 32%]
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 51%]
|
||||
........................................................................ [ 55%]
|
||||
........................................................................ [ 60%]
|
||||
........................................................................ [ 65%]
|
||||
........................................................................ [ 69%]
|
||||
........................................................................ [ 74%]
|
||||
........................................................................ [ 79%]
|
||||
........................................................................ [ 83%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 93%]
|
||||
........................................................................ [ 97%]
|
||||
................................... [100%]
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
================================ tests coverage ================================
|
||||
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||
|
||||
Name Stmts Miss Cover
|
||||
-----------------------------------------------
|
||||
app/__init__.py 1 0 100%
|
||||
app/api/__init__.py 0 0 100%
|
||||
app/api/auth.py 22 0 100%
|
||||
app/api/chat.py 177 0 100%
|
||||
app/api/chats.py 110 0 100%
|
||||
app/api/config.py 7 0 100%
|
||||
app/api/doc_drafts.py 93 0 100%
|
||||
app/api/docs.py 50 0 100%
|
||||
app/api/git_sources.py 212 0 100%
|
||||
app/api/health.py 10 0 100%
|
||||
app/api/steering.py 42 0 100%
|
||||
app/api/suggestions.py 8 0 100%
|
||||
app/api/sync.py 101 0 100%
|
||||
app/config.py 140 0 100%
|
||||
app/core/__init__.py 0 0 100%
|
||||
app/core/auth.py 20 0 100%
|
||||
app/core/caching.py 108 0 100%
|
||||
app/core/debugging.py 29 2 93%
|
||||
app/core/docs_push.py 39 0 100%
|
||||
app/core/logging.py 13 0 100%
|
||||
app/db.py 21 0 100%
|
||||
app/main.py 52 0 100%
|
||||
app/models.py 86 0 100%
|
||||
app/rag/__init__.py 0 0 100%
|
||||
app/rag/agent.py 222 0 100%
|
||||
app/rag/archive_upload.py 128 0 100%
|
||||
app/rag/chunker.py 206 4 98%
|
||||
app/rag/git_sources.py 14 0 100%
|
||||
app/rag/importer.py 180 3 98%
|
||||
app/rag/llm.py 216 0 100%
|
||||
app/rag/overview.py 71 0 100%
|
||||
app/rag/prompts.py 88 0 100%
|
||||
app/rag/retriever.py 150 3 98%
|
||||
app/rag/scaffolding.py 55 0 100%
|
||||
app/rag/source_removal.py 41 0 100%
|
||||
app/rag/sources_meta.py 16 0 100%
|
||||
app/rag/suggestions.py 27 0 100%
|
||||
app/rag/summarizer.py 24 0 100%
|
||||
app/schemas.py 210 0 100%
|
||||
-----------------------------------------------
|
||||
TOTAL 2989 12 99%
|
||||
coverage gate: app/ 99% (>90%) OK
|
||||
All checks passed!
|
||||
0 errors, 0 warnings, 0 informations
|
||||
validation OK
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
All gates green and the diff is exactly the specified change. Final report:
|
||||
|
||||
## Task 02 complete — client sends conversation history (with thinking)
|
||||
|
||||
**Implemented**
|
||||
- `frontend/assets/app.js` `runTurn` (single funnel for fresh send / phase-49 retry / phase-53 stale-regen): builds `history = conversation.slice(0, -1).map(...)` carrying exactly `{who, text, thinking}` (`thinking` only on brain records that streamed one — `undefined` drops the key) and sends `body: JSON.stringify({ message: text, history })`.
|
||||
- Verified the invariant: in all three paths the sent question is the LAST `conversation` entry at fetch time (fresh send pushes it; retry/stale-regen pop the old answer and keep the question last).
|
||||
- No UI change; no other files touched by this task (task 01's server work remains uncommitted in-tree for task 03's phase commit).
|
||||
|
||||
**Verification**
|
||||
- Wire check (throwaway Playwright script, run against real app + deterministic mock, then deleted): 10/10 PASS — turn 1 `history: []`; turn 2 history = first Q/A pair with 2.7k-char `thinking` on the brain entry and no extra keys; retry's history byte-identical to turn 2 (popped answer excluded, question not duplicated). Server logs show `history_msgs=0 → 2 → 2`.
|
||||
- `uv run pytest` → 1547 passed
|
||||
- `uv run pytest --cov=app --cov-report=term-missing` → 99% (>90%)
|
||||
- `uv run ruff check . && uv run pyright` → clean (0 errors)
|
||||
- E2E regressions in isolation: `test_chat_rag.py` 3 passed, `test_retry_answer.py` 4 passed, `test_thinking_display.py` 5 passed
|
||||
|
||||
**Decisions** — none; change matches the task spec verbatim. No commit (task 03 owns the phase commit).
|
||||
|
||||
**Next pending task:** `74_llm_chat_history/03_mock_marker_e2e.md` (mock `echo my history` marker + story E2E + commit).
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 9%]
|
||||
........................................................................ [ 13%]
|
||||
........................................................................ [ 18%]
|
||||
........................................................................ [ 23%]
|
||||
........................................................................ [ 27%]
|
||||
........................................................................ [ 32%]
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 51%]
|
||||
........................................................................ [ 55%]
|
||||
........................................................................ [ 60%]
|
||||
........................................................................ [ 65%]
|
||||
........................................................................ [ 69%]
|
||||
........................................................................ [ 74%]
|
||||
........................................................................ [ 79%]
|
||||
........................................................................ [ 83%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 93%]
|
||||
........................................................................ [ 97%]
|
||||
................................... [100%]
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
================================ tests coverage ================================
|
||||
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||
|
||||
Name Stmts Miss Cover
|
||||
-----------------------------------------------
|
||||
app/__init__.py 1 0 100%
|
||||
app/api/__init__.py 0 0 100%
|
||||
app/api/auth.py 22 0 100%
|
||||
app/api/chat.py 177 0 100%
|
||||
app/api/chats.py 110 0 100%
|
||||
app/api/config.py 7 0 100%
|
||||
app/api/doc_drafts.py 93 0 100%
|
||||
app/api/docs.py 50 0 100%
|
||||
app/api/git_sources.py 212 0 100%
|
||||
app/api/health.py 10 0 100%
|
||||
app/api/steering.py 42 0 100%
|
||||
app/api/suggestions.py 8 0 100%
|
||||
app/api/sync.py 101 0 100%
|
||||
app/config.py 140 0 100%
|
||||
app/core/__init__.py 0 0 100%
|
||||
app/core/auth.py 20 0 100%
|
||||
app/core/caching.py 108 0 100%
|
||||
app/core/debugging.py 29 2 93%
|
||||
app/core/docs_push.py 39 0 100%
|
||||
app/core/logging.py 13 0 100%
|
||||
app/db.py 21 0 100%
|
||||
app/main.py 52 0 100%
|
||||
app/models.py 86 0 100%
|
||||
app/rag/__init__.py 0 0 100%
|
||||
app/rag/agent.py 222 0 100%
|
||||
app/rag/archive_upload.py 128 0 100%
|
||||
app/rag/chunker.py 206 4 98%
|
||||
app/rag/git_sources.py 14 0 100%
|
||||
app/rag/importer.py 180 3 98%
|
||||
app/rag/llm.py 216 0 100%
|
||||
app/rag/overview.py 71 0 100%
|
||||
app/rag/prompts.py 88 0 100%
|
||||
app/rag/retriever.py 150 3 98%
|
||||
app/rag/scaffolding.py 55 0 100%
|
||||
app/rag/source_removal.py 41 0 100%
|
||||
app/rag/sources_meta.py 16 0 100%
|
||||
app/rag/suggestions.py 27 0 100%
|
||||
app/rag/summarizer.py 24 0 100%
|
||||
app/schemas.py 210 0 100%
|
||||
-----------------------------------------------
|
||||
TOTAL 2989 12 99%
|
||||
coverage gate: app/ 99% (>90%) OK
|
||||
All checks passed!
|
||||
0 errors, 0 warnings, 0 informations
|
||||
validation OK
|
||||
Reference in New Issue
Block a user