Repository navigation
Conversation
- Dedup chunks across the whole corpus instead of per article, so
syndicated copies of a story on different sites are caught
- Add nomic-embed-text task prefixes (search_document / search_query)
- Restore vector-search rank order before picking one chunk per article
in RAG (SQLite IN returned rows in arbitrary order)
- Outliers: one row per article, cluster filter applied server-side
- Sanitize LLM markdown with DOMPurify before {@html}
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pyproject.toml + uv.lock replace requirements.txt; Python pinned to 3.12
- backend/config.py: one pydantic-settings Settings object replaces
os.getenv calls scattered across modules
- FastAPI lifespan replaces deprecated on_event("startup")
- anthropic 0.34 -> 1.x; default model claude-sonnet-5-5. New
claude_client.complete() collects text blocks (current models think
by default, so content[0] may not be text), raises on refusal, and uses
server-side fallback on the Anthropic API. Label/query calls run at low
effort with room for thinking (max_tokens 20 would be consumed by it)
- lancedb 0.9 -> 0.40 (paginated list_tables), trafilatura 2.x,
openai, numpy 2, pandas, scikit-learn upgrades
- ruff config; zip(strict=True) so a short embedding batch fails loudly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Schema v2: jobs -> analyses; articles, chunks and projections carry an
analysis_id with ON DELETE CASCADE. Article ids hash analysis_id:url so
the same URL can appear in several analyses. WAL + busy timeout so the
pipeline thread and API reads don't block each other
- One LanceDB table per analysis; drop the unused cluster_id column and
its O(n^2) full-table rewrite on every run
- Migration from v1 turns the existing corpus into one analysis (keyed by
the latest finished job), backing up the DB and vectors to *.v1.bak.
Handles early v1 databases without the jobs.warning column
- Analyses left running by a server restart are marked as errors
- Split main.py into backend/db, backend/pipeline and the routes
- REST API under /api/analyses/{id}/...: list, get, delete (409 while
running), stream, corpus, clusters, outliers, articles, chat, export.
Pydantic response models so the frontend can generate a typed client
- Errors use string `detail`; the frontend was reading a key that never
existed, so every error showed as a bare "HTTP <status>"
- RAG raises instead of returning error text as an "answer"; chat maps
missing corpus to 409 and model/embedding failures to 502
- Old frontend pointed at the new routes until the rebuild
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pipeline.start/cancel keep a registry of running analyses. Cancellation
is cooperative: stages check a flag between queries, pages, articles,
embedding batches and cluster labels. Cancelled is a BaseException so
the stages' per-item `except Exception` handlers can't swallow it. A
cancelled run discards its partial data and ends in status 'cancelled'
- Each stage owns a progress band; stage_detail carries text such as
"Crawled 47 of 112 pages" (schema v3). Progress writes are throttled
- analysis_events table logs queries, indexed articles, skipped pages
with the reason (extractor now raises SkipArticle), clusters, warnings
and errors. SSE sends them as `log` events with ids so EventSource
resumes from Last-Event-ID; GET /events?after=N for polling
- POST /api/analyses/{id}/cancel; shutdown cancels running analyses
- Clear errors when search finds nothing or every page is filtered
- Commit each extracted article before reporting it: an open write
transaction made the event/progress writes hit "database is locked"
- Schema migration splits statements with comments stripped, so a ';'
in a SQL comment can't break it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spa_router serves frontend/build with an index.html fallback for client routes; registered after the API so it never shadows /api; refuses paths that resolve outside the build directory - Operation ids are route function names, so the generated client reads listAnalyses() rather than listAnalysesApiAnalysesGet() - scripts/export_openapi.py writes the schema for client generation - Query events read "query (16 new results)" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd 4 A fresh app replacing the single-page Svelte 4 UI. Structure - Real routes: / (topic input + every map), /analysis/[id] with Map, Timeline, Outliers, Key terms and Ask views. Cluster selection and the open article live in the URL, so views link and survive reloads - Typed API client generated from the backend's OpenAPI schema (@hey-api/openapi-ts); same-origin /api, proxied by Vite in dev - adapter-static SPA, served by FastAPI in production - bits-ui for accessible dialog/sheet/alert primitives; mode-watcher, svelte-sonner, lucide icons Design: a survey chart - Cool chart paper / night chart themes, Mona Sans with the width axis as the display voice, one accent for primary actions and focus - Map drawn with per-cluster density contours, direct ink labels with a colour swatch, zoom that survives selection changes, one scale for both axes, and a Table view as the accessible alternative - Categorical palette validated on both surfaces; the 8 largest clusters get hues, the rest share a neutral ink but keep their labels Behaviour - Live progress view: stage detail, progress, kept/skipped counts and the pipeline's event feed over SSE; cancel; failed/cancelled states with "map this topic again" - Articles take their majority cluster (and its mean position) instead of the first chunk's; outlier scores use the same model so every view names clusters consistently - Timeline as stacked bars per month or year (sparse data isn't interpolated); key terms ignore URLs - Chat history per map survives switching views; markdown sanitised with DOMPurify; citations open the in-app reader when the source is in the corpus Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chunks of one article are near-identical in embedding space, so clustering
chunks grouped text by source rather than by argument (HDBSCAN found ~one
"topic" per article). Topics are now found among articles.
- backend/topics.py: BERTopic's recipe without its PyTorch dependency.
Article vectors -> UMAP 5D -> HDBSCAN (leaf selection) -> merge splits
members can't tell apart -> noise joins a topic only if it would pass as
a member, else stays unclustered (-1) -> cap at 8 topics (the palette)
-> c-TF-IDF key terms (needing 2+ articles per term; boilerplate,
citation and month words filtered). Separate looser 2D UMAP for the map,
tuned on real corpora for ~2.5x more room per topic
- One Claude call names all topics with structured output, so names
contrast (the per-topic calls produced duplicates such as two "Urban
Heat Mitigation"); falls back to key terms if the call fails
- Schema v4: placements (one row per article: x, y, topic, outlier
distance) and clusters (label, description, size, terms) replace
chunk-level projections; v3 data converted on startup
- API: GET /analyses/{id}/articles and /clusters (with terms and
descriptions); /corpus and /outliers removed
- Whole-article near-duplicate detection (MinHash on word shingles)
catches syndicated copies whose intros differ; marked 'duplicate'
- scripts/recluster.py rebuilds maps from stored embeddings, no re-crawl
- complete() accepts a JSON schema (structured outputs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Load /articles and /clusters; drop client-side chunk aggregation and TF-IDF (corpus payload no longer carries chunk text) - Map draws one dot per article with contours per topic; unclustered articles shown in grey without a centre label; more padding for contours - Key terms page shows each topic's summary and c-TF-IDF terms - Cluster key shows the selected topic's summary Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- backend/synthesis/chat.py replaces rag.py: hybrid retrieval (vector + BM25 full-text, reciprocal-rank fusion; LanceDB FTS index built on first use), one excerpt per article, follow-up questions embed the previous question too - Citation numbers are stable across a conversation: the excerpts the previous answer cited are shown again under the same numbers and new excerpts continue the count. Without this, a follow-up's [6] meant a different article and the model couldn't re-check its own evidence - POST /chat streams server-sent events (sources, delta, done, error); GET /chat returns the saved conversation; DELETE /chat clears it. A turn is saved only when its answer completes, so a failure or a stopped answer leaves nothing half-written (schema v5: chat_messages) - stream_text() in the Claude client (server-side fallback kept); CHAT_EFFORT setting, default medium: measured first text in ~1s and ~9s per answer at both low and medium - Article dedup threshold 0.8 -> 0.5, with candidates confirmed by estimated Jaccard: across real maps, republished copies scored 0.52-1.0 (an edited WLRN reprint missed at 0.8 scored 0.77), distinct pages below 0.4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Answers stream in with a caret; sources appear as soon as retrieval
finishes ("Reading 8 articles…"); Stop ends an answer (nothing saved)
- [n] citations become small buttons (titled with the source) that open
the article reader; sources the answer cited are listed, the rest fold
under "Also searched, not cited"
- Conversation loads from the server, survives reloads, keeps streaming
if you switch views, and can be cleared
- fetch-based SSE reader (EventSource can't POST), unit-tested along with
citation linking
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Academic publishers serve headless browsers a bot check or paywall, so ~45% of crawled pages on research-heavy topics were discarded as too short. backend/ingestion/scholarly.py looks them up instead: - Identify the work from the URL (DOI, PMC, PubMed, Elsevier PII via Crossref, arXiv, MDPI's ISSN/volume/issue/article scheme) or citation meta tags - Text, best first: Europe PMC full text; open-access PDFs from OpenAlex locations (several tried: publisher PDFs are often blocked too, while repository copies work); else the abstract. PDF links the crawler skipped are read directly. Capped at 8,000 words per paper - Runs 8 lookups at a time after extraction, cancellable, with events; articles record where their text came from (source_note, schema v6) and the reader shows it. RECOVER_ACADEMIC=false turns it off Measured on real maps: 51 of 126 previously skipped pages recovered from the event logs; a fresh urban-heat crawl indexed 72 articles instead of 44 (36 of 47 academic pages recovered in 25 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scripts/seed_demo.py builds a small synthetic map (articles, topics with terms and summaries, activity log, a saved conversation) with no network or API keys: for trying the app, and as end-to-end test data - Playwright tests (frontend/e2e) drive the real backend serving the built app, seeded into a throwaway .e2e/ folder: home, map and key, topic selection across views, table and reader, timeline, key terms, saved chat with citations, theme, not-found - GitHub Actions: backend (uv, ruff, pytest), frontend (svelte-check, eslint, vitest, build), then e2e. npm pinned to 11 (npm 10's resolver crashes on this dependency tree) - .gitattributes keeps LF line endings across Windows and Linux Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- "Let me check the sources before they're read" pauses a run after searching: edit and re-run the searches, untick pages, add your own, then approve (new reviewing status, /plan endpoints, schema v7) - SEARCH_PROVIDER=searxng with SEARXNG_URL uses a self-hosted SearXNG instance instead of Brave Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EMBEDDING_MODE=fastembed (new default) runs nomic-embed-text v1.5 via
ONNX; vectors are normalised and match Ollama's (cosine 1.0000 on real
chunks, same speed on CPU), so existing maps stay searchable
- ollama keeps working ("local" is still accepted as its old name)
- Asking about a map embedded with a different-sized model now explains
which EMBEDDING_MODE to use instead of failing inside LanceDB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- On demand ("Find claims"): Claude lists up to 4 claims from each
article's opening (10 articles per request), then one request groups
claims from different articles into propositions, marking each claim
as agreeing, qualifying or disputing. Validated so every proposition
has two or more articles and each claim is used once.
- Runs in the background with progress on the analysis row (schema v8:
claims, propositions, proposition_claims); a restart marks it
interrupted. GET/POST /api/analyses/{id}/claims.
- New Claims tab: most debated (or most articles) first, stance bars on
one scale with counts written out, expand a point to read each
article's claim and open the article.
- Measured: $0.27 for a 52-article map, about a minute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UMAP's floating point differs between Windows and Linux; on CI one or two edge points stayed unclustered (by design), failing an assertion that every point be clustered. The test now requires the four topics, an exact split of the clustered points, and at least 90% clustered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An overhaul of Discourse Lens in six steps, one or more commits each. Every step was tested against real maps as well as the test suites.
What changes for users
localhost:8001, light and dark themes, works at phone width. Selection lives in the URL, so links and reloads land on the same view, topic and article.SEARCH_PROVIDER=searxng).Under the hood
claude-sonnet-5-5with structured outputs.Testing
uv run python -m scripts.seed_demoto explore it without API keys.Known gaps
EMBEDDING_MODEbetween OpenAI and the local models, maps made with the other one can't be searched. The app now says which setting to use instead of failing with a cryptic error.🤖 Generated with Claude Code