From 281f3555c3a24f82cde6664f1b526b7e4bf3d79f Mon Sep 17 00:00:00 2001 From: ducoterra Date: Mon, 31 Aug 2026 08:08:16 -0400 Subject: [PATCH] remove .agent, it shouldn't be committed. Do not commit it again --- .agent/PLAN.md | 521 ------------------ .../complete/52_pinned_composer/00_phase.md | 74 --- .../52_pinned_composer/01_sticky_composer.md | 46 -- .../02_e2e_pinned_composer.md | 37 -- .../01_sources_version_and_migration.md | 27 - .../02_sync_version_bump.md | 26 - .../03_chats_api_staleness.md | 23 - .../04_history_stale_badge.md | 23 - .../05_stale_banner_and_regenerate.md | 30 - .../06_e2e_stale_saved_chats.md | 28 - .../01_anonymous_save_share_api.md | 27 - .../55_save_share_ux/02_auto_save_default.md | 42 -- .../55_save_share_ux/03_share_for_everyone.md | 27 - .../55_save_share_ux/04_share_toast.md | 34 -- .../05_chat_actions_layout.md | 25 - .../phases/todo/55_save_share_ux/00_phase.md | 54 -- .../55_save_share_ux/06_e2e_save_share_ux.md | 33 -- 17 files changed, 1077 deletions(-) delete mode 100644 .agent/PLAN.md delete mode 100644 .agent/phases/complete/52_pinned_composer/00_phase.md delete mode 100644 .agent/phases/complete/52_pinned_composer/01_sticky_composer.md delete mode 100644 .agent/phases/complete/52_pinned_composer/02_e2e_pinned_composer.md delete mode 100644 .agent/phases/complete/53_stale_saved_chats/01_sources_version_and_migration.md delete mode 100644 .agent/phases/complete/53_stale_saved_chats/02_sync_version_bump.md delete mode 100644 .agent/phases/complete/53_stale_saved_chats/03_chats_api_staleness.md delete mode 100644 .agent/phases/complete/53_stale_saved_chats/04_history_stale_badge.md delete mode 100644 .agent/phases/complete/53_stale_saved_chats/05_stale_banner_and_regenerate.md delete mode 100644 .agent/phases/complete/53_stale_saved_chats/06_e2e_stale_saved_chats.md delete mode 100644 .agent/phases/complete/55_save_share_ux/01_anonymous_save_share_api.md delete mode 100644 .agent/phases/complete/55_save_share_ux/02_auto_save_default.md delete mode 100644 .agent/phases/complete/55_save_share_ux/03_share_for_everyone.md delete mode 100644 .agent/phases/complete/55_save_share_ux/04_share_toast.md delete mode 100644 .agent/phases/complete/55_save_share_ux/05_chat_actions_layout.md delete mode 100644 .agent/phases/todo/55_save_share_ux/00_phase.md delete mode 100644 .agent/phases/todo/55_save_share_ux/06_e2e_save_share_ux.md diff --git a/.agent/PLAN.md b/.agent/PLAN.md deleted file mode 100644 index 6077354..0000000 --- a/.agent/PLAN.md +++ /dev/null @@ -1,521 +0,0 @@ -# Brain of Reese — Master Plan - -> **Status:** Phase 1–3 complete (scaffolded, designed, decomposed). -> **Rule:** Every agent reads this file first. Decisions marked `LOCKED` in the -> Anchors table are settled — do not re-litigate them in a phase. -> **Revisions (2026-08-21, owner permission):** A7/A8/A9 revised (multi-format -> ingestion, hybrid FTS+vector retrieval, re-tuned honesty gate); dark tech -> theme (Phase 08); clickable document viewer (Phase 10); thinking display -> (Phase 17, owner permission 2026-08-23); follow-the-bottom scroll -> (Phase 18, owner choice 2026-08-23); shared header (Phase 19); -> whole-document context (Phase 24 — A7's 24k context cap removed, -> owner permission 2026-08-24). See roadmap §12. - ---- - -## 1. Mission - -A **knowledge base chatbot** that embeds the `~/Homelab` and `~/Deployments` -projects into a Postgres vector database and lets anyone ask *Reese* (the -bot) questions about them. - -**Product feel:** a chippy, upbeat assistant that is optimistic about the -user's ability ("you've got this") and **radically honest** — if retrieval -didn't surface anything relevant it says *"I haven't done anything like -that"* and offers alternatives instead of hallucinating. - -### In scope (v1) -- Chat UI (mobile-friendly, well-styled, no auth, no CDN). -- RAG over text knowledge files — `md, markdown, txt, yaml, yml, json, py` - by default (A9, revised 2026-08-21) — from `~/Homelab` + `~/Deployments` - (and any future directory the importer is pointed at). -- Self-hosted models via `https://aipi.reeseapps.com/v1` — `turbo` (chat), - `embed` (embeddings, **768 dims — verified**). -- Postgres 17 + pgvector, cosine similarity, chunk→document mapping so the - LLM receives the **entire relevant document** as context. -- Idempotent import/update script, documented in the README. -- Ample server logging + explicit UI loading/progress feedback (never a - stale submit button). - -### Out of scope (v1) -- Auth / multi-user (API is stateless under `/api` so it can be added later). -- Binary / non-text content, file uploads, caching layer, message persistence. -- Real-time document watching (manual re-import for now). - ---- - -## 2. Architectural Anchors (LOCKED DECISIONS) - -| # | Component | Decision | Rationale | Status | -|---|-----------|----------|-----------|--------| -| A1 | Runtime | Python 3.12+, `uv` for all package management | Fast, reproducible envs; one language for API + tooling | LOCKED | -| A2 | Web framework | FastAPI + Pydantic v2 + Uvicorn | Async, typed, SSE-friendly for LLM streaming, free OpenAPI docs | LOCKED | -| A3 | Database | **PostgreSQL 17** (`docker.io/postgres:17`, pgvector compiled in via `db/Containerfile`) with **cosine** (`<=>`) search | One system for relational + vectors; pgvector is mature; official base image kept per project standard | LOCKED | -| A4 | Orchestration | `compose.yaml`, started with **`podman compose up -d`** | Matches Reese's toolchain | LOCKED | -| A5 | LLM backend | OpenAI-compatible `https://aipi.reeseapps.com/v1`; models **`turbo`** (chat) & **`embed`** (embeddings); `openai` async client | Self-hosted, offline from cloud; no new model management | LOCKED | -| A6 | Embedding dim | **768** (verified 2026-08-21 against live endpoint via `scripts/llm_probe.py`); configured by `BOR_EMBEDDING_DIM` | User recalled 768 — probe confirmed; dimension is fixed at table creation, so mismatch must fail loudly at import time | LOCKED | -| A7 | Retrieval→context | **Hybrid:** cosine top-30 + Postgres FTS top-30 (OR tsquery, `ts_rank`) fused with **RRF (k=60)** → map to parent documents ranked by best fused chunk score → feed the **full text of top-N=2 documents** (deduped) to the LLM | Owner permission 2026-08-21: pure-cosine top-4 missed real docs (gitlab case — best chunk ranked 7th behind vendored-cache junk; score compression 0.41–0.84); the lexical signal finds name-your-tool questions; whole-document context contract preserved. A7 revised 2026-08-24 — matched documents never truncated (owner: "this should never happen"; emergency-valve variant rejected) | LOCKED (revised 2026-08-24) | -| A8 | Honesty gate | **Deflection mode** (LLM must open with a variant of *"I haven't done anything like that"* and offer 2–3 alternative questions) when best cosine < `BOR_RELEVANCE_THRESHOLD` **and** no candidate chunk FTS-matches the question; threshold re-tuned for the `embed` model's compressed score range (default **0.62**, calibrated via `scripts/eval_retrieval.py`; the E2E mock uses its own 0.30 calibration via the app fixture) | Owner permission 2026-08-21: at 0.30 the gate never discriminated (measured corpus range 0.41–0.84); the FTS-OR keeps name-your-tool questions honest-positive; deflection product behavior unchanged | LOCKED (revised 2026-08-21) | -| A9 | Content scope | Text formats **`md, markdown, txt, yaml, yml, json, py`** (default, `BOR_IMPORT_EXTENSIONS`), **hidden (dot) directories skipped by default**, plus the exclusion list (`node_modules`, `__pycache__`, `.pytest_cache`, `dist`, `build`, …) | Owner permission 2026-08-21: real notes live in yaml/py/json/txt too; the dot-dir skip removes the ~470 vendored-cache junk docs (`.esphome/.espressif/**`, …) that outranked real content | LOCKED (revised 2026-08-27) | -| A10 | Auth | **None in v1**; all endpoints stateless under `/api` | Per user (auth later); statelessness keeps the future migration cheap | LOCKED | -| A11 | Frontend | Vanilla HTML/CSS/JS in git; **no CDN** — everything served by FastAPI `StaticFiles`; minified by esbuild in the `Containerfile` build stage; system font stack | No external deps at runtime; tiny, auditable surface; mobile-friendly by construction | LOCKED | -| A12 | Aux services | **None in v1** (no Valkey, no SeaweedFS) | No sessions/auth (no store), no uploads (no object storage); add later only if a need appears | LOCKED | -| A13 | Migrations | Alembic + SQLAlchemy 2.0 (sync) + psycopg 3 | Standard, reversible, reviewable schema history | LOCKED | -| A14 | Debugging | `debugpy` **only when `DEBUGPY=1`** (env var read directly, not via settings); listen `0.0.0.0:5678` (override `DEBUGPY_PORT`), non-blocking, attach-on-demand; **not imported at all when off** | Zero overhead by default per project standard; attach-on-demand keeps production runs clean | LOCKED | -| A15 | Chat transport | **SSE streaming** from `POST /api/chat` (deltas + final `done` event with metadata) | Local LLM latency is 10–30s; live token stream + explicit completion event power the UI's feedback states | LOCKED | -| A16 | Testing | Per phase: unit + integration (pytest, **coverage >90%** on `app/`) + **one dedicated Playwright E2E file per user story**, run in isolation; E2E uses a deterministic mock LLM by default (`E2E_REAL_LLM=1` opts into live aipi) | One story, one phase, one E2E gate — the pipeline's core invariant | LOCKED | -| A17 | Git | Conventional Commits, **always `--no-gpg-sign`**, repo-local `commit.gpgsign=false`; one atomic commit per completed phase | Subsequent agents may lack the GPG key | LOCKED | - -> **A10 revision (phase 16, owner permission 2026-08-22):** single-admin -> signed-cookie auth — public: chat / documents / suggestions / health; -> admin-only: docs catalog + steering. The row above keeps the original v1 -> decision text; the public API surface stays stateless (the signed -> session cookie is the only session state) — recorded as a revision, -> not a silent deviation. -> -> **A10 UI revision (phase 19, owner permission 2026-08-23):** the -> "Sources" nav link is hidden from anonymous users on all pages — the -> soft-gate page and the API split above are unchanged. -> -> **A7 revision (phase 24, owner permission 2026-08-24):** the -> `[…truncated…]` cap on document context is removed — -> `select_documents` always returns the full top-N texts; -> `BOR_MAX_CONTEXT_CHARS` is gone. The steering section -> (`BOR_STEERING_MAX_CHARS`, phase 15) keeps its budget and the shared -> marker. -> -> **A9 revision (phase 47, owner permission 2026-08-27):** the format -> set extends with the Podman quadlet family (`container, network, -> volume, image, pod, kube, swap, os, endpoint`) and `j2` (Jinja -> templates) — plain-text chunking (`chunk_text`), owner: `TODO.md` -> L10–L11. The narrow-only `BOR_IMPORT_EXTENSIONS` rule and the -> hidden-dir/exclusion invariants are unchanged. - ---- - -## 3. High-Level Architecture - -``` - ┌────────────────────────────────────────────┐ - │ Podman Compose │ - Browser │ ┌──────────────────────────────────────┐ │ - ┌──────────┐ HTTP │ │ brain-of-reese/app (FastAPI) │ │ - │ index.html│◄──────┼─►│ • static frontend (no CDN) │ │ - │ app.js │ SSE │ │ • /api/chat /api/suggestions │ │ - └──────────┘ │ │ • /api/health /api/docs │ │ - │ │ • RAG pipeline (embed→retrieve→gen) │ │ - │ └──────┬──────────────────┬───────────┘ │ - │ │ SQL (psycopg) │ OpenAI-compat│ - │ ┌──────▼──────┐ ┌───────▼────────────┐ │ - │ │ db: │ └─────────┬──────────┘ │ - │ │ postgres:17 │ │ │ - │ │ + pgvector │ │ │ - │ └─────────────┘ │ │ - └──────────────────────────────┼────────────┘ - ▼ - https://aipi.reeseapps.com/v1 - (self-hosted: turbo, embed) - - Offline tooling (same repo, same venv): - scripts/import_docs.py → walks A9-format dirs, chunks, embeds, upserts - scripts/eval_retrieval.py → ranks hybrid results for a question (tuning) - scripts/llm_probe.py → verifies models + embedding dim -``` - -### Component breakdown -| Component | Responsibility | Lives in | -|-----------|----------------|----------| -| **App (FastAPI)** | Serves frontend + `/api`; RAG pipeline; logging | `app/` | -| **RAG pipeline** | `embed` → pgvector cosine top-K → doc mapping → context assembly → `turbo` (streamed) with persona/honesty prompt | `app/rag/` (added in story phases) | -| **Importer** | Directory walk (A9 formats, hidden dirs skipped, exclusions), sha256 delta detection, format-aware chunking, batched embedding, upsert/prune | `scripts/import_docs.py` (story phase) | -| **DB** | `documents`, `chunks`, `query_log` + `vector` extension | `db/` image, `alembic/` | -| **Frontend** | Chat shell, sources view, loading/feedback states | `frontend/` | - -### Chat data flow -``` -user question - → POST /api/chat {message} - → embed(question) [aipi /v1/embeddings, model=embed] - → cosine top-30 + FTS top-30 (OR tsquery, ts_rank) [pgvector + PG FTS] - → RRF fuse (k=60) → docs ranked by best fused chunk score - ├─ best cosine >= 0.62 OR fts_hits > 0 → top-2 documents' FULL content - │ → system prompt (persona + HONESTY rules + docs) - │ → turbo, stream=True → SSE deltas - └─ else → DEFLECT_MODE system prompt (weak hits as topics) - → turbo, stream=True → SSE deltas (honest reply) - → query_log row (question, score, deflected, sources, latency) - → final SSE "done" event: {deflected, sources[], suggestions[]} -``` - ---- - -## 4. API Design - -All endpoints stateless (A10). Errors: standard JSON `{detail: str}`. - -| Method | Path | Purpose | Story | -|--------|------|---------|-------| -| GET | `/api/health` | Liveness + db up/down + version | 01 | -| GET | `/api/suggestions` | Onboarding suggestion strings | 01 (05 refines) | -| GET | `/api/docs` | Indexed document list (source, path, title, chunks, indexed_at) | 02 | -| GET | `/api/documents/content?source=…&path=…` | One indexed document's full content (feeds the viewer page) | 10 | -| POST | `/api/chat` | RAG chat turn → **SSE stream** | 03/04 | - -### SSE contract (`POST /api/chat`) -``` -data: {"type":"thinking","text":"…"}\n\n -data: {"type":"thinking","text":"…"}\n\n -data: {"type":"delta","text":"Hey! "}\n\n -data: {"type":"delta","text":"Good "}\n\n -... -data: {"type":"done","deflected":false,"sources":[{"source":"Homelab","path":"kubernetes.md","title":"Kubernetes Homelab Cluster"}],"suggestions":[]}\n\n -``` -Client rules: render deltas as they arrive; render `thinking` text in a -collapsible block above the answer; auto-collapse on the first `delta`; -tolerate interleaved `thinking` events (append — never reopen once the -answer started); the `done` shape is unchanged (thinking never travels on -`done`); on `done` append source chips / suggestion chips and clear the -busy state; on HTTP/stream error show the error banner + retry (never a -stuck button). - -> **SSE revision (phase 17, owner permission 2026-08-23):** the contract -> gains one event type — `{"type":"thinking","text":"…"}` — carrying the -> model's reasoning ahead of the `delta` events (the `turbo` model emits -> `delta.reasoning_content` chunks before the first content chunk, verified -> live 2026-08-23; `BOR_STREAM_THINKING=0` suppresses the frames -> server-side). `delta` and `done` shapes are unchanged — a recorded -> extension of A15, not a silent deviation. -> -> **SSE revision (phase 37, owner permission 2026-08-26):** the contract -> gains a second event type — `{"type":"tool","name":"…","argument":…}` — -> carrying the model's document tool calls on grounded turns (phase 37: -> `list_documents` / `read_document`, budgeted by `BOR_AGENT_LIST_CALLS` -> / `BOR_AGENT_READ_CALLS` (removed in phase 45 — see the revision note -> below); `argument` is `"source/path"` for -> `read_document`, null otherwise). Client rule: render each `tool` frame -> as a "calling tool" line/state (task 05); `delta` and `done` shapes are -> unchanged — the read document is reflected in `done.sources` instead -> (deduped) — a recorded extension of A15, not a silent deviation. -> -> **SSE revision (phase 45, owner permission 2026-08-27):** the phase-37 -> per-turn tool budgets are **removed** (owner: "allow the LLM to make -> as many tool calls as it wants — `TODO.md` L8): `BOR_AGENT_LIST_CALLS` -> / `BOR_AGENT_READ_CALLS` no longer exist; `BOR_AGENT_MAX_ROUNDS` -> (default 10) caps the tool rounds and `0` disables the tools -> entirely (the pre-phase-37 path). The `tool` event shape and the -> `done` shape are unchanged — a recorded revision of the phase-37 -> note's budget wording, not a silent deviation. - ---- - -## 5. Data Model (PostgreSQL 17) - -Created by `alembic/versions/0001_initial_schema.py` (idempotent -`CREATE EXTENSION IF NOT EXISTS vector`). - -### `documents` -| Column | Type | Notes | -|--------|------|-------| -| id | `UUID` PK | | -| source | `VARCHAR(120)` | source dir basename, e.g. `Homelab` | -| path | `VARCHAR(1000)` | relative to source dir, e.g. `ansible/roles/k3s.md` | -| full_path | `VARCHAR(2000)` | absolute path at import time (diagnostics) | -| title | `VARCHAR(500)` | first markdown H1, else file stem | -| content | `TEXT` | **full markdown — the RAG context** | -| content_hash | `VARCHAR(64)` | sha256 of content — change detection | -| indexed_at | `TIMESTAMPTZ` | | -| — | `UNIQUE (source, path)` | upsert key | - -### `chunks` -| Column | Type | Notes | -|--------|------|-------| -| id | `UUID` PK | | -| document_id | `UUID` FK→documents CASCADE | **embedding→document mapping** | -| position | `INT` | 0-based order within the doc | -| content | `TEXT` | chunk text (heading-aware) | -| embedding | `VECTOR(768)` | nullable until embedded (two-phase import) | -| tsv | `TSVECTOR` | **generated** `to_tsvector('english', content) STORED` + GIN index (hybrid retrieval, A7) | - -> No vector index in v1: sequential scan is fine at this corpus size -> (~100–500 docs). Revisit with an HNSW index if retrieval latency grows. - -### `query_log` -`id UUID PK, question TEXT, top_score FLOAT, fts_hits INT, chunk_hits INT, deflected BOOL, sources TEXT, latency_ms INT, created_at TIMESTAMPTZ` - -### Document state transitions -``` -unseen ──import──▶ indexed ──hash changed + re-import──▶ reindexed - │ - └──file deleted + --prune──▶ removed (chunks cascade) -``` - -### Chunking policy (markdown-aware) -Split on `## `/`### ` headings into sections; sub-split any section longer -than `BOR_CHUNK_TARGET_CHARS` (2000) at paragraph boundaries with -`BOR_CHUNK_OVERLAP_CHARS` (200) overlap; each chunk keeps its nearest -preceding heading in the text for retrieval quality. - -**Format-aware (A9, revised):** `yaml`/`yml` split on top-level keys and -`---` separators (key line kept as anchor); `json` pretty-printed, split on -top-level keys; `py` split on top-level defs/classes (stdlib `ast`); -`txt` on paragraphs; markdown unchanged. The quadlet family -(`container, network, volume, image, pod, kube, swap, os, endpoint`) -and `j2` (Jinja templates) are plain-text chunked — no format-specific -splitter (A9 revised 2026-08-27, phase 47). Every format honors the -1200-char hard cap (aipi ~1024-token request limit). - ---- - -## 6. RAG Pipeline & Persona - -### Locked system prompt (sent with every chat turn) -``` -You are "Brain of Reese" — the digital brain of Reese, a self-hoster and -homelab tinkerer. Personality: chippy, upbeat, warm, and genuinely -optimistic about the user's ability to do things ("you've got this"). - -Rules: -1. Answer ONLY from the provided document context. Cite which document(s) - you used, by path. -2. Be concrete: names, versions, ports, hosts, schedules — the specifics in - the docs are the value. -3. HONESTY GATE: if is "LOW", you must NOT pretend to know. - Start your answer with a variant of: "I haven't done anything like that." - Then offer 2-3 alternative questions about things you DO have notes on. -4. Never invent facts, hosts, or steps that are not in the context. -5. Keep answers tight: short paragraphs, bullets where helpful. - -{HIGH|LOW} -``` -- `HIGH` mode appends the full document text under `…`. -- `LOW` mode (deflection) appends only the **titles** of the weak hits so the - model can suggest real alternatives (marker used by the E2E mock: - `DEFLECT_MODE` appears in the system prompt). - -### Retrieval (hybrid — A7/A8, revised 2026-08-21) -- Embed the question (`embed`, 768-d) → cosine top-30 candidates. -- Lexical: OR tsquery over the question's tokens → FTS top-30 by `ts_rank`. -- **Reciprocal Rank Fusion** (`Σ 1/(k+rank)`, k=60) → distinct parent docs - ranked by best chunk's fused score → top 2 → full content, concatenated - — **never truncated** (A7 revised, phase 24, owner permission - 2026-08-24). -- Honesty gate: LOW only when `best cosine < BOR_RELEVANCE_THRESHOLD` - (default 0.62, calibrated against the `embed` model's measured 0.41–0.84 - distribution) **and** zero FTS hits among the candidates. - ---- - -## 7. UI/UX Strategy - -### 7.1 Layout structure -- **App frame:** sticky header (64px) + `
` (flex-grow) + footer. - Container: `max-width: 72rem; margin-inline: auto; padding-inline: 1.25rem`. -- **Shared header (Phase 19, owner permission 2026-08-23):** the bar is a - shared contract across chat / sources / viewer — one bar per page, same - controls (brand + nav [Chat, Sources — admin only] + New Chat + Sign in - / Sign out on chat & sources; the viewer bar = back + title + the same - actions in `.doc-header-actions`). One shared module - (`frontend/assets/header.js`) toggles the existing controls on each - page, so they can never "disappear" between pages again; the heights - stay pinned at 64px / 58px (phase 12). -- **Chat:** a *centered column capped at 46rem*. This is deliberate: chat is - a vertical conversation — a centered, capped column is the correct pattern - (NOT a layout bug). The 72rem frame + header/footer ensure the column - never reads as a hairline in a sea of whitespace. -- **Sources page:** full-width responsive **table** (min 640px, horizontal - scroll wrapper on small screens) + stat cards in - `grid-template-columns: repeat(auto-fit, minmax(170px, 1fr))`. - No skinny single-column lists anywhere: lists/tables/grids use ≥80–90% of - the container width. -- **Mobile (≤640px):** suggestion chips become a horizontally scrollable row; - composer stays reachable with `safe-area-inset-bottom`; touch targets ≥44px. - -### 7.2 Accessibility (WCAG 2.1 AA) -- Semantic landmarks on every page: `
`, `