Skip to content

Add step_timer profiler and refactor consensus algorithm - #1

Open
nevil-mathew wants to merge 52 commits into
mainfrom
knn-to-ann
Open

Add step_timer profiler and refactor consensus algorithm#1
nevil-mathew wants to merge 52 commits into
mainfrom
knn-to-ann

Conversation

@nevil-mathew

@nevil-mathew nevil-mathew commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Introduced the GraphWeave package with graph-based consensus clustering as the default, weighted data support, GPU acceleration, adaptive kNN backends, and cumulative batch processing.
    • Added LLM capabilities for labeling, topic merging, granularity tuning, embedding adaptation, keyphrase expansion, and low-confidence corrections.
    • Added visualization, reporting, quote verification, benchmarking, and model save/load support.
  • Documentation
    • Rebranded and extensively updated user guides, configuration references, examples, troubleshooting, and performance guidance.
  • Chores
    • Added CI workflows, packaging metadata, optional installation extras, and licensing notices.
  • Tests
    • Added broad unit, integration, cumulative-clustering, adaptation, and benchmark coverage.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: afb7cb9a-9741-456f-98d7-40d428fe286e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

TriTopic 2.3.0 adds a step_timer timing utility (with psutil support), instruments core pipeline and graph construction with timed blocks, propagates verbose flags to components, refactors graph-based consensus to sparse upper-triangle accumulation with early pruning and limited parallelism, extends inference APIs to accept precomputed embeddings, and updates README guidance.

Changes

Timing and memory profiling infrastructure

Layer / File(s) Summary
step_timer utility module and dependency
tritopic/utils/timing.py, tritopic/utils/__init__.py, pyproject.toml
New step_timer context manager reports elapsed time and memory deltas (uses psutil/resource when available). Exported from tritopic.utils. psutil>=5.9.0 added.

Verbose parameter support across components

Layer / File(s) Summary
EmbeddingEngine verbose parameter and model-load timing
tritopic/core/embeddings.py
EmbeddingEngine.__init__ accepts verbose: bool = False and uses it to control step_timer("model-load", ...) and an optional loading print.
Verbose parameter forwarding in TriTopic initialization
tritopic/core/clustering.py, tritopic/core/model.py
ConsensusLeiden gains verbose; TriTopic forwards self.config.verbose into EmbeddingEngine and ConsensusLeiden during initialization.

Consensus Leiden algorithm optimization

Layer / File(s) Summary
Consensus run parallelism capping and timing
tritopic/core/clustering.py
fit_predict caps parallel_jobs to min(n_jobs, 4) and wraps Leiden runs and consensus computation in step_timer blocks controlled by verbose.
Graph-based consensus algorithm refactor with sparse accumulation
tritopic/core/clustering.py
_compute_consensus (graph mode) enumerates intra-cluster pairs per run to build upper-triangle counts, computes integer threshold_count from consensus_threshold_tau, prunes pairs early, optionally calls libc.malloc_trim(0), and emits (rows, cols, freq). _consensus_via_leiden_on_graph now accepts these arrays, builds an igraph with weight=freq, thresholds edges, and runs Leiden inside step_timer.

Pipeline execution timing instrumentation

Layer / File(s) Summary
Model fitting pipeline timing and progress logging
tritopic/core/model.py
TriTopic.fit captures start time and wraps encoding, TF-IDF matrix construction, keywords extraction, graph building, Leiden clustering, and dimensionality reduction in step_timer contexts; total fit time is logged.
Graph construction timing instrumentation
tritopic/core/graph_builder.py
Graph-builder phases (exact-kNN, HNSW build/query, mutual-kNN, SNN, lexical-kNN, semantic selection, lexical adjacency, overlap-bonus, adjacency→igraph) are wrapped in step_timer blocks and include a few verbose prints.

Public inference API extensions

Layer / File(s) Summary
encode() method and optional embeddings support
tritopic/core/model.py
Adds TriTopic.encode(documents) for direct embedding computation. transform() and transform_proba() accept optional `embeddings: np.ndarray

Documentation updates

Layer / File(s) Summary
README configuration, memory optimization, and troubleshooting updates
README.md
Clarifies low_memory applies only to hierarchical consensus (graph mode ignores it), documents float32 accumulation and per-run pruning, states Leiden thread cap, updates tuning guidance and the OOM troubleshooting row for graph-mode-specific knobs (n_consensus_runs, consensus_threshold_tau).

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 Hop, hop! Through the timing gates we run,
Consensus sparse and swift—the algorithm's done!
Embeddings optional, profiles in sight,
TriTopic 2.3 shines so bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title Check ✅ Passed Title check skipped as CodeRabbit has written the PR title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch knn-to-ann

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Add step_timer profiler and refactor consensus algorithm Jun 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 399: The guidance is inverted: update the README entries that mention
consensus_threshold_tau and n_consensus_runs so they recommend raising
consensus_threshold_tau (stricter filtering, higher τ) and/or lowering
n_consensus_runs (e.g., 5) to reduce memory/OOM risk rather than lowering τ;
locate and edit the table row text that currently reads "Lower
`n_consensus_runs` ... or lower `consensus_threshold_tau` ..." and change it to
suggest raising `consensus_threshold_tau` (e.g., 0.3 → use a higher value) and
lowering `n_consensus_runs`, and apply the same wording change to the other
occurrence referenced in the comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0e38867-effa-40fd-b4b1-bd2111ce867e

📥 Commits

Reviewing files that changed from the base of the PR and between ee74259 and c0cb265.

📒 Files selected for processing (8)
  • README.md
  • pyproject.toml
  • tritopic/core/clustering.py
  • tritopic/core/embeddings.py
  • tritopic/core/graph_builder.py
  • tritopic/core/model.py
  • tritopic/utils/__init__.py
  • tritopic/utils/timing.py

Comment thread README.md Outdated
Comment on lines 161 to 217
co_occur = None
for partition in partitions:
unique_ids = np.unique(partition)
cluster_map = {cid: idx for idx, cid in enumerate(unique_ids)}
cols = np.array([cluster_map[c] for c in partition])
rows = np.arange(n_nodes)
data = np.ones(n_nodes, dtype=ones_dtype)
M = sp_csr((data, (rows, cols)), shape=(n_nodes, len(unique_ids)))
# M @ M.T is the co-membership matrix for this partition (sparse)
co_run = M.dot(M.T)
if co_occur is None:
co_occur = co_run
else:
co_occur = co_occur + co_run
for r_idx, partition in enumerate(partitions):
# Group node indices by cluster for this run.
cluster_to_nodes: dict[int, list[int]] = {}
for node_idx, cluster_id in enumerate(partition):
cluster_to_nodes.setdefault(int(cluster_id), []).append(node_idx)

# Enumerate upper-triangle pairs (i < j, no diagonal) within each cluster.
# Upper triangle only: the downstream consumer at line 307 reads only
# coo.row < coo.col, so lower-triangle and diagonal entries are always
# discarded — no point computing or storing them.
# This replaces the M @ M.T approach: same counts, no M matrix, no
# intermediate co_run dense step, half the pairs to store.
run_rows: list[np.ndarray] = []
run_cols: list[np.ndarray] = []
for members in cluster_to_nodes.values():
if len(members) < 2:
continue
m = np.sort(np.asarray(members, dtype=np.int32))
ii, jj = np.triu_indices(len(m), k=1) # k=1 skips diagonal
run_rows.append(m[ii])
run_cols.append(m[jj])

if not run_rows:
continue

r = np.concatenate(run_rows)
c = np.concatenate(run_cols)
del run_rows, run_cols
# int16: counts ∈ [0, n_runs ≤ 32k], 2 bytes vs float32's 4 bytes.
# Each node is in exactly one cluster per run, so no duplicate (r,c)
# pairs exist within a single run — .tocsr() handles conversion cleanly.
co_run = sp_coo(
(np.ones(len(r), dtype=np.int16), (r, c)),
shape=(n_nodes, n_nodes),
).tocsr()
del r, c

co_occur = co_run if co_occur is None else co_occur + co_run

# Early pruning: after run r_idx, the max a pair can still reach is
# current_count + runs_remaining. Drop pairs whose ceiling falls
# below threshold_count — they can never survive the final cut.
runs_remaining = n_runs - r_idx - 1
min_reachable = threshold_count - runs_remaining
if min_reachable > 1:
co_occur.data[co_occur.data < min_reachable] = 0
co_occur.eliminate_zeros()

if self.consensus_method == "graph":
coo = co_occur.tocoo()
rows, cols = coo.row, coo.col
freq = coo.data.astype(np.float64) / float(n_runs)
del coo, co_occur
return self._consensus_via_leiden_on_graph(
co_occur, n_nodes, n_runs, partitions
rows, cols, freq, n_nodes, n_runs, partitions
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle edge case where co_occur remains None.

If every Leiden partition produces only singleton clusters, co_occur stays None throughout the loop. Line 211 (co_occur.tocoo()) and the hierarchical path (lines 224, 228) will crash with AttributeError.

This is pathological but possible with very high resolution or disconnected graphs. Add a guard before accessing co_occur.

🛡️ Proposed fix
+        # Guard against degenerate case where no pairs were ever co-clustered.
+        if co_occur is None:
+            # Fallback: pick the partition with highest mean ARI.
+            best_score = -1.0
+            best = partitions[0]
+            for p in partitions:
+                avg = float(np.mean([adjusted_rand_score(p, q) for q in partitions]))
+                if avg > best_score:
+                    best_score = avg
+                    best = p
+            return best
+
         if self.consensus_method == "graph":
             coo = co_occur.tocoo()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
co_occur = None
for partition in partitions:
unique_ids = np.unique(partition)
cluster_map = {cid: idx for idx, cid in enumerate(unique_ids)}
cols = np.array([cluster_map[c] for c in partition])
rows = np.arange(n_nodes)
data = np.ones(n_nodes, dtype=ones_dtype)
M = sp_csr((data, (rows, cols)), shape=(n_nodes, len(unique_ids)))
# M @ M.T is the co-membership matrix for this partition (sparse)
co_run = M.dot(M.T)
if co_occur is None:
co_occur = co_run
else:
co_occur = co_occur + co_run
for r_idx, partition in enumerate(partitions):
# Group node indices by cluster for this run.
cluster_to_nodes: dict[int, list[int]] = {}
for node_idx, cluster_id in enumerate(partition):
cluster_to_nodes.setdefault(int(cluster_id), []).append(node_idx)
# Enumerate upper-triangle pairs (i < j, no diagonal) within each cluster.
# Upper triangle only: the downstream consumer at line 307 reads only
# coo.row < coo.col, so lower-triangle and diagonal entries are always
# discarded — no point computing or storing them.
# This replaces the M @ M.T approach: same counts, no M matrix, no
# intermediate co_run dense step, half the pairs to store.
run_rows: list[np.ndarray] = []
run_cols: list[np.ndarray] = []
for members in cluster_to_nodes.values():
if len(members) < 2:
continue
m = np.sort(np.asarray(members, dtype=np.int32))
ii, jj = np.triu_indices(len(m), k=1) # k=1 skips diagonal
run_rows.append(m[ii])
run_cols.append(m[jj])
if not run_rows:
continue
r = np.concatenate(run_rows)
c = np.concatenate(run_cols)
del run_rows, run_cols
# int16: counts ∈ [0, n_runs ≤ 32k], 2 bytes vs float32's 4 bytes.
# Each node is in exactly one cluster per run, so no duplicate (r,c)
# pairs exist within a single run — .tocsr() handles conversion cleanly.
co_run = sp_coo(
(np.ones(len(r), dtype=np.int16), (r, c)),
shape=(n_nodes, n_nodes),
).tocsr()
del r, c
co_occur = co_run if co_occur is None else co_occur + co_run
# Early pruning: after run r_idx, the max a pair can still reach is
# current_count + runs_remaining. Drop pairs whose ceiling falls
# below threshold_count — they can never survive the final cut.
runs_remaining = n_runs - r_idx - 1
min_reachable = threshold_count - runs_remaining
if min_reachable > 1:
co_occur.data[co_occur.data < min_reachable] = 0
co_occur.eliminate_zeros()
if self.consensus_method == "graph":
coo = co_occur.tocoo()
rows, cols = coo.row, coo.col
freq = coo.data.astype(np.float64) / float(n_runs)
del coo, co_occur
return self._consensus_via_leiden_on_graph(
co_occur, n_nodes, n_runs, partitions
rows, cols, freq, n_nodes, n_runs, partitions
)
co_occur = None
for r_idx, partition in enumerate(partitions):
# Group node indices by cluster for this run.
cluster_to_nodes: dict[int, list[int]] = {}
for node_idx, cluster_id in enumerate(partition):
cluster_to_nodes.setdefault(int(cluster_id), []).append(node_idx)
# Enumerate upper-triangle pairs (i < j, no diagonal) within each cluster.
# Upper triangle only: the downstream consumer at line 307 reads only
# coo.row < coo.col, so lower-triangle and diagonal entries are always
# discarded — no point computing or storing them.
# This replaces the M @ M.T approach: same counts, no M matrix, no
# intermediate co_run dense step, half the pairs to store.
run_rows: list[np.ndarray] = []
run_cols: list[np.ndarray] = []
for members in cluster_to_nodes.values():
if len(members) < 2:
continue
m = np.sort(np.asarray(members, dtype=np.int32))
ii, jj = np.triu_indices(len(m), k=1) # k=1 skips diagonal
run_rows.append(m[ii])
run_cols.append(m[jj])
if not run_rows:
continue
r = np.concatenate(run_rows)
c = np.concatenate(run_cols)
del run_rows, run_cols
# int16: counts ∈ [0, n_runs ≤ 32k], 2 bytes vs float32's 4 bytes.
# Each node is in exactly one cluster per run, so no duplicate (r,c)
# pairs exist within a single run — .tocsr() handles conversion cleanly.
co_run = sp_coo(
(np.ones(len(r), dtype=np.int16), (r, c)),
shape=(n_nodes, n_nodes),
).tocsr()
del r, c
co_occur = co_run if co_occur is None else co_occur + co_run
# Early pruning: after run r_idx, the max a pair can still reach is
# current_count + runs_remaining. Drop pairs whose ceiling falls
# below threshold_count — they can never survive the final cut.
runs_remaining = n_runs - r_idx - 1
min_reachable = threshold_count - runs_remaining
if min_reachable > 1:
co_occur.data[co_occur.data < min_reachable] = 0
co_occur.eliminate_zeros()
# Guard against degenerate case where no pairs were ever co-clustered.
if co_occur is None:
# Fallback: pick the partition with highest mean ARI.
best_score = -1.0
best = partitions[0]
for p in partitions:
avg = float(np.mean([adjusted_rand_score(p, q) for q in partitions]))
if avg > best_score:
best_score = avg
best = p
return best
if self.consensus_method == "graph":
coo = co_occur.tocoo()
rows, cols = coo.row, coo.col
freq = coo.data.astype(np.float64) / float(n_runs)
del coo, co_occur
return self._consensus_via_leiden_on_graph(
rows, cols, freq, n_nodes, n_runs, partitions
)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tritopic/core/clustering.py (1)

61-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate consensus_threshold_tau when constructing the clusterer.

The config contract says this is a fraction in [0, 1], but out-of-range values currently fail open: tau > 1 prunes every edge and forces the fallback partition, while tau < 0 admits every observed pair. Rejecting invalid input here keeps both consensus paths predictable.

Proposed fix
         if consensus_method not in ("graph", "hierarchical"):
             raise ValueError(
                 f"consensus_method must be 'graph' or 'hierarchical', got {consensus_method!r}"
             )
+        if not 0.0 <= consensus_threshold_tau <= 1.0:
+            raise ValueError(
+                f"consensus_threshold_tau must be in [0, 1], got {consensus_threshold_tau!r}"
+            )
         self.consensus_method = consensus_method
         self.consensus_threshold_tau = consensus_threshold_tau
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tritopic/core/clustering.py` around lines 61 - 66, The constructor currently
assigns consensus_threshold_tau without validation; add a check in the
initializer after validating consensus_method to ensure consensus_threshold_tau
is a number within [0, 1] (e.g., isinstance check and 0.0 <=
consensus_threshold_tau <= 1.0) and raise ValueError with a clear message if it
is out of range or not numeric; update the assignment to
self.consensus_threshold_tau only after the validation so that consensus paths
remain predictable (refer to the consensus_method and consensus_threshold_tau
parameters and the class initializer where they are set).
♻️ Duplicate comments (1)
tritopic/core/clustering.py (1)

210-223: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the empty co-occurrence case before dereferencing co_occur.

If every Leiden run yields only singleton clusters, the loop never materializes co_occur, and Line 217 still calls tocoo() on None. The hierarchical branch has the same failure mode, so the new graph fallback never gets a chance to run.

Proposed fix
+        if co_occur is None:
+            best_fallback_score = -1.0
+            best = partitions[0]
+            for p in partitions:
+                avg = float(np.mean([adjusted_rand_score(p, q) for q in partitions]))
+                if avg > best_fallback_score:
+                    best_fallback_score = avg
+                    best = p
+            return best
+
         try:
             import ctypes
             ctypes.cdll.LoadLibrary("libc.so.6").malloc_trim(0)
         except (OSError, AttributeError):
             pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tritopic/core/clustering.py` around lines 210 - 223, The code dereferences
co_occur without checking for an empty/None result — guard the empty
co-occurrence case by checking if co_occur is None or has no nonzero entries
(e.g., co_occur is None or co_occur.nnz == 0) before calling co_occur.tocoo();
if empty, skip the tocoo()/Leiden path and invoke the graph fallback (same
behavior needed for the hierarchical branch), ensuring the function returns or
falls back to _consensus_via_leiden_on_graph only when co_occur contains data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tritopic/core/clustering.py`:
- Around line 61-66: The constructor currently assigns consensus_threshold_tau
without validation; add a check in the initializer after validating
consensus_method to ensure consensus_threshold_tau is a number within [0, 1]
(e.g., isinstance check and 0.0 <= consensus_threshold_tau <= 1.0) and raise
ValueError with a clear message if it is out of range or not numeric; update the
assignment to self.consensus_threshold_tau only after the validation so that
consensus paths remain predictable (refer to the consensus_method and
consensus_threshold_tau parameters and the class initializer where they are
set).

---

Duplicate comments:
In `@tritopic/core/clustering.py`:
- Around line 210-223: The code dereferences co_occur without checking for an
empty/None result — guard the empty co-occurrence case by checking if co_occur
is None or has no nonzero entries (e.g., co_occur is None or co_occur.nnz == 0)
before calling co_occur.tocoo(); if empty, skip the tocoo()/Leiden path and
invoke the graph fallback (same behavior needed for the hierarchical branch),
ensuring the function returns or falls back to _consensus_via_leiden_on_graph
only when co_occur contains data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a1d6d46-cbc8-4cad-867b-e9de2239d2f7

📥 Commits

Reviewing files that changed from the base of the PR and between c0cb265 and bc03787.

📒 Files selected for processing (1)
  • tritopic/core/clustering.py

nevil-mathew and others added 17 commits June 4, 2026 12:56
- Introduced CumulativeTriTopic class for cumulative, batch-wise clustering on top of TriTopic.
- Added configuration options for reclustering strategies, triggers, and memory management.
- Implemented batch processing with automatic reclustering based on novelty detection and manual triggers.
- Created StreamingCorpus utility for generating realistic streaming datasets with LSA embeddings.
- Developed evaluation metrics for comparing cumulative clustering against full-batch baselines, including ARI, NMI, and keyword overlap.
- Added pluggable recluster strategies: global_refit, coreset, and batch_merge.
- Enhanced metrics utility with functions for computing ARI, NMI, and Jaccard overlap for keyword stability.
- Introduced GPU acceleration for compute-intensive steps using PyTorch, FAISS, and RAPIDS cuML.
- Updated README.md to include GPU installation instructions and performance benefits.
- Added `min_cluster_fraction` parameter to TriTopicConfig for better scaling with corpus size.
- Refactored clustering methods to utilize effective min_cluster_size based on document count.
- Implemented GPU-accelerated cosine similarity and embedding refinement functions.
- Updated cumulative benchmark notebook to use new configuration options.
- Enhanced graph builder to select between FAISS and HNSW backends based on sample size.
- Introduced LLM-based topic alignment in `alignment.py` with functions `_build_align_prompt` and `_parse_alignment_response`.
- Added `llm_align_topics` function to facilitate LLM-driven topic alignment, allowing for flexible mapping of new topics to existing global topics.
- Updated `CumulativeTriTopic` to support LLM alignment methods, including "cosine", "llm", and "both", with appropriate configuration options.
- Implemented stratified coreset selection in `strategies.py` to ensure representation of rare topics, enhancing robustness against tail-collapse.
- Added metrics for evaluating rare topic recall in `evaluation.py`, providing insights into the preservation of small topics during cumulative modeling.
- Enhanced `LLMLabeler` to support OpenRouter as a provider, allowing for broader compatibility with LLM services.
- Implemented `llm_merge_topics` method in `TriTopic` and `CumulativeTriTopic` classes to semantically merge topics using a large language model (LLM).
- Introduced a new module `llm_merger.py` for handling LLM-driven topic merging, including prompt building and response parsing.
- Enhanced `LLMLabeler` with structured output capabilities for LLM calls, allowing for better integration with Google and OpenAI APIs.
- Added JSON schema enforcement for structured responses from the LLM, improving the reliability of the merging process.
- Updated documentation and added warnings for potential stale states in the cumulative model after merging.
- Implemented sensitivity weights for lightweight-coreset sampling, favoring outliers.
- Added tests for sensitivity sampling and micro-cluster coreset strategies.
- Enhanced README with details on new coreset selection methods: sensitivity and microcluster.
- Updated coreset configuration options to include `coreset_sampling` and `reserve_novel_docs`.
Weighted coresets (stratified_coreset/sensitivity_weights) previously
only reached small-cluster pruning, centroids, and keyword extraction —
the Leiden partition objective itself drew cluster boundaries as if
every sampled point represented one document, blunting the point of
the provably-bounded sampling distributions upstream.

ConsensusLeiden now switches to RBERVertexPartition with
node_sizes=node_weights whenever node weights are present, scoped only
to that branch so every unweighted fit (including global_refit) keeps
using RBConfigurationVertexPartition exactly as before, with zero
behavior change. An earlier attempt that scaled edge weights by w_i*w_j
instead of switching objectives was discarded after benchmarking showed
it amplifies kNN-graph noise near sparse regions and measurably hurt
real coreset fidelity (ARI vs. full-batch baseline dropped); the native
node_sizes mechanism avoids that failure mode and raised ARI from
~0.88-0.95 to ~0.92-0.99 across seeds on a realistic streaming benchmark.
Adds a third, opt-in path for choosing the Leiden resolution parameter
alongside modularity-maximization and binary-search-to-target-count.
Follows ClusterLLM (Zhang, Wang & Shang, EMNLP 2023): candidate
resolutions are scored by how well their partitions agree with LLM
judgments on sampled same/different-cluster document triplets.

- tritopic/labeling/llm_granularity.py: triplet sampling, prompt
  building, tiered-fallback response parsing, and candidate scoring
- TriTopic.tune_resolution_with_llm(): callable after fit(), reuses
  the existing graph/refresh helpers, never invoked automatically
- Unit + integration tests with a mocked labeler (no real API calls)
- README section documenting the method and its cost profile

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LaDUfD19BrzSkqbv1bfqbx
- Thread node_weights through llm_select_resolution/_partition_at_resolution
  so candidate scoring uses the same RBER+node_sizes objective as the final
  weighted consensus re-fit (previously always unweighted RBConfiguration)
- Guard tune_resolution_with_llm against models missing graph_/documents_/
  embeddings_ (e.g. after save()/load(), which never persists graph_) with
  a clear error instead of a deep leidenalg crash
- Validate n_candidates/batch_size are >= 1 in llm_select_resolution
- Drop unused NearestNeighbors distance outputs in _sample_triplets
- Laplace-smooth _triplet_agreement so a candidate informative on only one
  or two triplets can't outrank one informative across many at a slightly
  lower but more reliable agreement rate
- Replace np.argmax's implicit first-index tie-break with an explicit
  middle-candidate fallback for tied/all-zero scores
- Persist the calibrated resolution back to config.resolution and
  _clusterer.resolution so later resolution-dependent defaults
  (build_hierarchy, divide, _auto_resolve_topic_count) stay consistent

Adds 10 new tests covering each fix; all verified empirically before
being asserted (e.g. Leiden partition invariance for the tie-break test).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LaDUfD19BrzSkqbv1bfqbx
Verifies the node_weights forwarding fixed in the previous commit
(model.py:1871) actually reaches leidenalg end-to-end as the weighted
RBER objective across the full call chain (candidate scoring, per-run
consensus, and the co-occurrence re-clustering step), not just as a
passed-but-unused kwarg.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LaDUfD19BrzSkqbv1bfqbx
New section 5b (opt-in, off by default via TUNE_RESOLUTION_WITH_LLM)
demonstrates tune_resolution_with_llm() as an alternative to manually
hand-tuning RESOLUTION. Uses the existing OpenRouter labeler pattern
already established in this notebook (OPENROUTER_MODEL defaults to
google/gemini-2.5-flash-lite; DeepSeek is equally suitable) rather than
Claude, since B-or-C triplet judgments don't need a stronger model.

Outputs stripped before commit to match the other notebooks in this
repo and avoid committing real dataset content into git history.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LaDUfD19BrzSkqbv1bfqbx
…ation-9c6nrj

Add LLM-guided granularity calibration (ClusterLLM-style)
nevil-mathew and others added 26 commits July 2, 2026 10:58
- Added a check for empty content in the LLM labeler, raising a ValueError with a descriptive message if the response is empty.
- Updated the LLM merger to issue a warning and return all topics as singletons when the LLM response is empty or consists only of whitespace.
- Introduced a minimum cluster size parameter to suppress small clusters during candidate partitioning, aligning with final fit behavior.
- Improved triplet sampling efficiency with a global k-NN query, reducing computational cost.
- Added diagnostics to capture detailed metrics during resolution selection, including triplet counts and unparsed responses.
- Implemented a two-stage resolution selection process: a coarse sweep followed by a fine grid search around the best candidate.
- Enhanced triplet response parsing to handle unparsed responses more robustly, avoiding bias in scoring.
- Updated documentation to reflect new features and improvements in the LLM granularity approach.
- Introduced a new feature to allow LLM-guided calibration of the RESOLUTION parameter.
- Added markdown explanation for the new feature, detailing its benefits and usage.
- Implemented code to utilize the LLM for tuning resolution based on boundary case judgments.
- Included diagnostics output to assess the effectiveness of the calibration.
…ation docs

New tritopic.adaptation subpackage implementing ClusterLLM-style triplet
fine-tuning: sample LLM-judged (anchor, positive, negative) triplets from a
fitted model, adapt the embedder to them via a pure-numpy linear transform
or a real sentence-transformers fine-tune, and refit TriTopic on the result.
Also includes the cheaper Few-Shot-Clustering extras (LLM keyphrase
expansion, low-confidence correction) and a compare_embedders evaluation
harness for judging whether adaptation actually helped on a given corpus.

- tritopic/adaptation/: config, triplet sampling/bank/cache, LinearAdapter +
  EmbeddingAdapter, evaluation harness, keyphrase/correction extras,
  adapt_and_refit() pipeline
- TriTopic.adapt_embeddings_with_llm(): in-place delegate mirroring
  tune_resolution_with_llm's ergonomics
- benchmarks/adaptation_quality_report.py: oracle-labeler POC report
  (synthetic + real 20 Newsgroups scenarios)
- notebooks/embedding_adaptation_demo.ipynb: executed end-to-end demo/test
  notebook with inline assertions, real 20NG data
- 34 new tests across tests/test_adaptation_*.py
- pyproject.toml: new `adaptation` extra (datasets, accelerate)
- README: document the new feature; expand tune_resolution_with_llm docs
  with two-stage/bias-mitigation details and a diagnostics example

Co-Authored-By: Claude Fable 5 <[email protected]>
- run_benchmark.py: reproduces the README's TriTopic vs BERTopic/NMF/LDA
  benchmark table (20ng, BBC News, AG News, Arxiv) with a --quick synthetic
  smoke-test mode for CI. The file was previously referenced by the README
  but never committed because a blanket `run*` .gitignore rule silently
  swallowed it; added a `!run_benchmark.py` exception.
- Verify quoted phrases in LLM-generated report-theme narratives against
  the source documents shown to the LLM (tritopic/utils/quote_verification.py),
  flagging quotes that can't be traced back to a real document so a fabricated
  participant quote doesn't silently ship in a qualitative research report.
  Surfaced via ReportTheme.unverified_quotes and an inline warning in
  export_report()'s Markdown output.
- Add GitHub Actions CI: pytest on push/PR (py3.10/3.12), a fast benchmark
  harness smoke test, and a manually-triggered full benchmark reproduction job.
- Fix README code samples using the nonexistent `n_topics_target` kwarg;
  the actual TriTopic constructor param is `n_topics`.
…ules

CI (.github/workflows/ci.yml):
- persist-credentials: false on all checkout steps
- add a read-only top-level permissions block
- timeout-minutes on the full-benchmark job

run_benchmark.py:
- embedding cache key now includes sample_seed, so a rerun with a different
  subsampling seed can't silently reuse embeddings for a different set of docs

tritopic/adaptation/adapter.py:
- _resolve_mode now runs the same accelerate/datasets/sentence-transformers
  version check for explicit adapter_mode="finetune" that the "auto" branch
  already ran, raising the same actionable message instead of failing later
  with a raw ImportError out of _finetune_sentence_transformer
- EmbeddingAdapter.encode()'s normalize flag is now threaded through to
  LinearAdapter.transform() in linear mode instead of being ignored

tritopic/adaptation/correction.py:
- reassign_low_confidence drops any topic that a reassignment empties out
  entirely before recomputing centroids/probabilities, instead of averaging
  an empty embedding slice (was a real NaN-propagation crash, reproduced and
  confirmed fixed with a new regression test)
- clarify batch_size's docstring: it chunks iteration only, doesn't batch
  LLM requests (kept the signature as-is; a real multi-doc-per-call rewrite
  is a bigger change than this pass warrants)

tritopic/adaptation/keyphrase.py:
- generate_keyphrases' cache loader now skips malformed/partial JSONL lines
  instead of aborting the whole read
- keyphrase_expand_embeddings no longer blends in encode("") for documents
  with an empty keyphrase list; also merges the doc+keyphrase encode calls
  into one, halving request overhead for API-backed encoders

tritopic/adaptation/pipeline.py:
- adapt_and_refit carries the original model's explicit n_topics through to
  the refit model instead of silently resetting it to "auto"
- warns when use_metadata_view was enabled, since the fit-time metadata
  DataFrame isn't persisted on the model and can't be recovered for the refit

tritopic/core/model.py:
- AdaptationConfig is now imported under TYPE_CHECKING so the annotation on
  adapt_embeddings_with_llm resolves for static analysis (was a bare
  "name not defined" for mypy/Ruff)

Skipped (not still valid / not worth the risk-to-value tradeoff):
- Renaming/removing correction.py's batch_size outright — no call site or
  doc example depends on the "real batching" framing; docstring fix covers
  the misleading-cost-expectation concern without an API break.

All 232 tests pass (224 pre-existing + 8 new regression tests), including a
reproduction of the correction.py crash against the pre-fix code to confirm
the new test actually exercises the bug.
HF's datasets/huggingface_hub stopped resolving the legacy script-based
"ag_news" repo id (HfUriError: repo id must be namespace/name), breaking
the full benchmark CI job. Point at fancyzhx/ag_news, the parquet-based
mirror with the same text/label schema.
The manual full-benchmark job (4 datasets x 3 seeds x k-grid x 4 models,
including BERTopic) was hitting the 45-minute timeout and getting
cancelled before finishing, not failing outright.
…mark-error-juhs58

Update AG News dataset source to fancyzhx/ag_news
…teness-ipnli2

Add LLM-guided embedding adaptation module; polish granularity calibration docs
Mirrors the CSV_PATHS config pattern from challenges_clustering_kaggle.ipynb:
set CSV_PATHS (or the CSV_PATHS/CSV_PATH env var) to load your own documents
from CSV instead of the built-in 20 Newsgroups demo. An optional LABEL_COL
enables the ground-truth oracle self-test in Section 3; without it, that
section is skipped and the notebook points to the real-LLM path in Section 4.
…-input-kvq90t

Add LLM-guided embedding adaptation module; polish granularity calibration docs
- Added a new notebook `embedding_adaptation_kaggle.ipynb` for real fine-tuning on Kaggle using OpenRouter and `all-MiniLM-L6-v2`.
- Updated `README.md` to include links to the new notebook and clarify the adaptation process.
- Modified `adapt_and_refit` function to include the trained `EmbeddingAdapter` in the report, allowing for persistence of fine-tuned weights.
- Added a test to ensure the `EmbeddingAdapter` can be saved and loaded correctly after adaptation.
- Updated CHEAT_SHEET.md to reflect default memory-safe configuration for large datasets.
- Changed README.md to specify file locations in a clearer format.
- Adjusted README.md to correct the OOM crash mitigation advice.
- Modified __init__.py to reorder exports for better organization.
- Enhanced graphweave_full_demo.ipynb by clarifying LLM labeling options and comments.
- Improved run_benchmark.py to provide clearer warnings when BERTopic is not installed.
- Updated CHEAT_SHEET.md to clarify memory usage for default consensus_method="graph" and legacy hierarchical path.
- Revised README.md to emphasize the memory-safe nature of the default path and provide clearer installation instructions for optional features.
- Enhanced VISUAL_GUIDE.md to reflect changes in memory usage timelines and consensus steps for both paths.
- Adjusted integration tests to remove unnecessary low_memory=True settings when using the default graph consensus.
- Modified model configuration to clarify the effect of low_memory on hierarchical consensus only.
- Updated cumulative benchmark notebook to reflect changes in consensus method and memory management.
Introduce the GraphWeave brand and top-level API
… improve error handling in adaptation modules

- Updated CI to support Python 3.9 in addition to 3.10 and 3.12.
- Clarified installation instructions in README for LLM-guided embedding fine-tuning.
- Improved error handling in adapter.py and correction.py to provide more informative warnings.
- Enhanced keyphrase.py to include normalization options for embeddings.
- Updated triplet sampling logic for better performance and clarity.
- Added tests for quote verification and reasoning field handling in LLM labeler.
- Changed co-occurrence matrix accumulation from float32 to int16 to optimize memory usage.
- Updated documentation to reflect changes in data types and memory safety.
- Modified error handling in `adapt_and_refit` to raise ValueError when metadata is not passed, ensuring metadata view is preserved.
- Added tests to verify that metadata is correctly handled during adaptation and refitting processes.
- Adjusted installation instructions in notebooks to point to the correct repository branch.
Update embedding adaptation, metrics, CI, and GraphWeave metadata
Add LLM-guided embedding adaptation and report quote verification

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
graphweave/core/graph_builder.py (1)

78-113: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Keep FAISS from preempting hnswlib in the adaptive kNN path.

_select_knn_backend returns faiss_gpu/faiss_cpu before the hnswlib import branch, so with FAISS available any corpus at/above hnsw_small_threshold uses the FAISS path. FAISS IndexFlatIP is exact brute-force inner-product search; it does not preserve the documented/Sub-quadratic HNSW fallback (5k–~50khnsw_small, ≥50khnsw_large with its HNSW tuning). Gate FAISS to an appropriate size band and fall back through hnswlib above it to keep the adaptive behavior intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/core/graph_builder.py` around lines 78 - 113, Update
_select_knn_backend so FAISS is selected only within its intended size band,
rather than for every corpus at or above hnsw_small_threshold. For larger
corpora, continue through the hnswlib availability and threshold checks so
hnsw_small and hnsw_large retain their documented adaptive behavior, while
preserving GPU-over-CPU preference within the allowed FAISS range.
🧹 Nitpick comments (7)
graphweave/cumulative/evaluation.py (1)

128-138: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Keep the OOM warning, but avoid km.transform as the fix.

The (emb[:, None, :] - centers[None, :, :]) ** 2 broadcast creates an (n_samples, n_clusters, n_features) temporary for the full corpus, which can exhaust RAM. km.transform(emb) only produces an (n_samples, n_clusters) distance matrix, which can still be too large. Use chunked nearest-center scoring instead, for example via pairwise_distances_argmin_min(..., metric='euclidean', return_distance=True) and accumulate scores without building the full (N, k, d) array.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/cumulative/evaluation.py` around lines 128 - 138, Update
_distortion to avoid both broadcasted (n_samples, n_clusters, n_features)
allocations and km.transform; compute nearest-center distances in chunks using
pairwise_distances_argmin_min or an equivalent chunked approach, and accumulate
weighted or unweighted distortion scores incrementally. Preserve the existing
full_cost/work_cost ratio and OOM warning behavior.

Source: Linters/SAST tools

graphweave/adaptation/pipeline.py (1)

148-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Validate metadata before the expensive adaptation work.

This use_metadata_view precondition is only checked after collect_triplets() (billable LLM calls) and adapter.finetune() (training) have already run at Lines 124–129. A caller who forgets metadata= pays the full cost only to hit a ValueError. Move the check up right after config is resolved so it fails fast.

♻️ Proposed reordering
     config = config or AdaptationConfig()
+    if model.config.use_metadata_view and metadata is None:
+        raise ValueError(
+            "adapt_and_refit: the original model used use_metadata_view=True, but "
+            "the fit-time metadata DataFrame is not persisted on the model, so it "
+            "can't be reconstructed automatically. Pass the same metadata used for "
+            "the original fit() call via the metadata= argument to preserve the "
+            "metadata view on refit."
+        )
     documents = model.documents_

Then delete the original block at Lines 148–155.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/adaptation/pipeline.py` around lines 148 - 155, Move the
use_metadata_view/metadata precondition check in adapt_and_refit immediately
after config is resolved, before collect_triplets() or adapter.finetune()
execute, and remove the later duplicate check while preserving the existing
ValueError message and behavior.
graphweave/labeling/llm_labeler.py (1)

104-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_reasoning_unsupported_models is process-wide shared state.

Defining this as a class attribute means every LLMLabeler instance mutates the same set. Two labelers pointing at different base_urls but using the same model id would share rejection memory, which could suppress the reasoning-toggle attempt on an endpoint that actually supports it. The docstring frames this as intentional caching, so this is only worth revisiting if per-endpoint isolation matters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/labeling/llm_labeler.py` around lines 104 - 107, The
_reasoning_unsupported_models cache is shared across all LLMLabeler instances,
allowing one endpoint’s rejection to affect another. Move this state from the
class attribute into each LLMLabeler instance’s initialization, and update the
relevant methods to use the instance-owned set while preserving the existing
model-level caching behavior per endpoint.
benchmarks/adaptation_quality_report.py (1)

163-167: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve target_names once instead of re-loading the full corpus per category.

The inner fetch_20newsgroups(subset="all") runs once for every entry in cats, so the entire ~18k-doc corpus is loaded and parsed 5 times (plus the outer call) only to map category indices to names. target_names doesn't depend on the categories filter, so hoist it out.

♻️ Proposed fix
 cats = [0, 1, 2, 3, 4]  # 5 categories, kept small for a fast local run
+    target_names = fetch_20newsgroups(subset="all").target_names
     data = fetch_20newsgroups(
         subset="all",
-        categories=[fetch_20newsgroups(subset="all").target_names[c] for c in cats],
+        categories=[target_names[c] for c in cats],
         remove=("headers", "footers", "quotes"),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/adaptation_quality_report.py` around lines 163 - 167, Resolve the
20 Newsgroups target names once before constructing the categories argument,
then reuse that value when mapping each entry in cats. Update the data-loading
flow around fetch_20newsgroups so the full corpus is not fetched inside the
per-category list comprehension, while preserving the existing category
selection and outer fetch behavior.
notebooks/coreset_shorttext_test.ipynb (1)

477-495: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Diagnostics section reaches into strategies.py internals.

ReclusterContext/make_strategy("coreset") are constructed directly here to mirror the model's internal coreset-building logic. This duplicates several constructor parameters inline and will silently drift if the internal strategy signature changes. Acceptable for a benchmark notebook's diagnostics, but consider exposing a small public helper (e.g. CumulativeGraphWeave.debug_working_set()) if this kind of introspection is needed elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/coreset_shorttext_test.ipynb` around lines 477 - 495, Expose a
public diagnostics helper on CumulativeGraphWeave, such as debug_working_set(),
that rebuilds and returns the final coreset working set using the model’s
current configuration and state. Update the notebook diagnostics to call this
helper instead of constructing ReclusterContext and make_strategy("coreset")
directly, while preserving the existing ratio and reporting behavior.
tests/test_integration_20ng.py (1)

30-34: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve target_names once instead of re-fetching per category.

fetch_20newsgroups(subset="all") is invoked once for every element of cats (10–12 full-bunch reconstructions) purely to map indices → names, then again for data. Even with disk caching this re-parses the whole corpus repeatedly on each fixture build.

♻️ Fetch once, reuse names
 def _ng20(n_docs: int, cats: list[int], seed: int = 42) -> tuple[list[str], np.ndarray]:
     """Load a balanced sample of 20NG and return (documents, labels)."""
+    all_names = fetch_20newsgroups(subset="all").target_names
     data = fetch_20newsgroups(
         subset="all",
-        categories=[fetch_20newsgroups(subset="all").target_names[c] for c in cats],
+        categories=[all_names[c] for c in cats],
         remove=("headers", "footers", "quotes"),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_integration_20ng.py` around lines 30 - 34, Update the 20
Newsgroups fixture setup to fetch the full dataset once, store its target_names,
and reuse those names when converting cats to category names before constructing
data. Replace the per-category fetches inside the categories comprehension while
preserving the existing subset and removal options.
graphweave/core/model.py (1)

762-767: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

GPU _refine_embeddings fallback swallows failures silently.

Unlike the FAISS/HNSW fallbacks in graph_builder.py (which print a diagnostic when verbose), this except Exception: pass gives no signal at all if gpu_refine_embeddings fails, making GPU-path issues hard to diagnose even with verbose=True.

♻️ Suggested fix
         try:
             from graphweave.utils.gpu import gpu_refine_embeddings
             return gpu_refine_embeddings(original_embeddings, labels, blend_factor)
-        except Exception:
-            pass
+        except Exception as exc:
+            if self.config.verbose:
+                print(f"      GPU refine failed ({type(exc).__name__}: {exc}); falling back to CPU.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/core/model.py` around lines 762 - 767, Update the GPU fallback in
_refine_embeddings so exceptions from gpu_refine_embeddings emit a diagnostic
when verbose is enabled, matching the FAISS/HNSW fallback behavior; retain the
existing fallback flow after logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 24: Update the Python version matrix in the CI workflow to include the
declared-supported versions 3.11 and 3.13, while preserving the existing 3.9,
3.10, and 3.12 entries so CI covers the full supported 3.9–3.13 range.

In `@graphweave/core/model.py`:
- Around line 806-814: Update the UMAP backend selection in the
dimensionality-reduction flow to require an explicit configuration or CLI opt-in
before importing and using cuML; otherwise always use the CPU umap backend. Add
the corresponding configuration/CLI documentation describing backend selection
and cuML reproducibility caveats, while preserving the existing random-state
behavior.
- Around line 348-369: Update _effective_min_cluster_size and divide so weighted
thresholds use only the documents represented by the current subgraph, not the
full sample_weights_ total. In divide, derive the subgraph’s corresponding
weights, use their summed mass when calculating the threshold, and pass those
same node weights to sub_clusterer.fit_predict so pruning uses consistent
weighted units; preserve existing full-corpus behavior for other callers.

In `@graphweave/utils/metrics.py`:
- Around line 261-266: Update the classification branch around the cv
calculation and cross_val_score call to handle a rarest-class count below two
explicitly, avoiding cross-validation with cv=1 and returning the appropriate
degenerate-case score. Preserve the existing cross-validated F1 behavior when cv
is at least 2.

In `@notebooks/challenges_clustering_kaggle.ipynb`:
- Line 44: Update the notebook’s pip install cell to use the intended public
GraphWeave repository and branch directly, replacing the unresolved
topic-extraction-poc source while preserving the graphweave extras and quiet
installation options.

In `@notebooks/coreset_shorttext_test.ipynb`:
- Line 42: Update the notebook’s GraphWeave pip install command to replace the
unavailable “rework” Git branch with a stable release, tag, or commit SHA that
exists in the repository. Keep the existing extras, repository URL, quiet
output, and filtering behavior unchanged.

In `@notebooks/cumulative_graphweave_benchmark.ipynb`:
- Line 39: Update all notebook installation commands to use the GraphWeave
repository on `@main` or a released tag instead of transient branches. Change
notebooks/cumulative_graphweave_benchmark.ipynb:39 and
notebooks/graphweave_full_demo.ipynb:40 from GraphWeave.git@rework; replace the
old topic-extraction-poc references at
notebooks/embedding_adaptation_demo.ipynb:77 and
notebooks/embedding_adaptation_kaggle.ipynb:44 with the GraphWeave repository on
`@main` or a released tag.

In `@PKG-INFO`:
- Around line 8-10: Regenerate PKG-INFO from the current pyproject.toml so its
project URLs, CI badge metadata, and adaptation extra—including
sentence-transformers>=3.0—match the source configuration; alternatively, remove
PKG-INFO from version control to prevent future drift.

In `@tests/test_integration_20ng.py`:
- Around line 78-90: Update the docstring and nearby comments for the drift
fixture to match cats_old = [1, 2, 3, 4, 5] and cats_new = [9, 10]: describe
five established comp.* categories followed by two emerging rec.sport.*
categories, and remove the incorrect sci.* and eight/four-category claims.

---

Outside diff comments:
In `@graphweave/core/graph_builder.py`:
- Around line 78-113: Update _select_knn_backend so FAISS is selected only
within its intended size band, rather than for every corpus at or above
hnsw_small_threshold. For larger corpora, continue through the hnswlib
availability and threshold checks so hnsw_small and hnsw_large retain their
documented adaptive behavior, while preserving GPU-over-CPU preference within
the allowed FAISS range.

---

Nitpick comments:
In `@benchmarks/adaptation_quality_report.py`:
- Around line 163-167: Resolve the 20 Newsgroups target names once before
constructing the categories argument, then reuse that value when mapping each
entry in cats. Update the data-loading flow around fetch_20newsgroups so the
full corpus is not fetched inside the per-category list comprehension, while
preserving the existing category selection and outer fetch behavior.

In `@graphweave/adaptation/pipeline.py`:
- Around line 148-155: Move the use_metadata_view/metadata precondition check in
adapt_and_refit immediately after config is resolved, before collect_triplets()
or adapter.finetune() execute, and remove the later duplicate check while
preserving the existing ValueError message and behavior.

In `@graphweave/core/model.py`:
- Around line 762-767: Update the GPU fallback in _refine_embeddings so
exceptions from gpu_refine_embeddings emit a diagnostic when verbose is enabled,
matching the FAISS/HNSW fallback behavior; retain the existing fallback flow
after logging.

In `@graphweave/cumulative/evaluation.py`:
- Around line 128-138: Update _distortion to avoid both broadcasted (n_samples,
n_clusters, n_features) allocations and km.transform; compute nearest-center
distances in chunks using pairwise_distances_argmin_min or an equivalent chunked
approach, and accumulate weighted or unweighted distortion scores incrementally.
Preserve the existing full_cost/work_cost ratio and OOM warning behavior.

In `@graphweave/labeling/llm_labeler.py`:
- Around line 104-107: The _reasoning_unsupported_models cache is shared across
all LLMLabeler instances, allowing one endpoint’s rejection to affect another.
Move this state from the class attribute into each LLMLabeler instance’s
initialization, and update the relevant methods to use the instance-owned set
while preserving the existing model-level caching behavior per endpoint.

In `@notebooks/coreset_shorttext_test.ipynb`:
- Around line 477-495: Expose a public diagnostics helper on
CumulativeGraphWeave, such as debug_working_set(), that rebuilds and returns the
final coreset working set using the model’s current configuration and state.
Update the notebook diagnostics to call this helper instead of constructing
ReclusterContext and make_strategy("coreset") directly, while preserving the
existing ratio and reporting behavior.

In `@tests/test_integration_20ng.py`:
- Around line 30-34: Update the 20 Newsgroups fixture setup to fetch the full
dataset once, store its target_names, and reuse those names when converting cats
to category names before constructing data. Replace the per-category fetches
inside the categories comprehension while preserving the existing subset and
removal options.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee272079-5331-403b-a26b-f7d6cdb2e15a

📥 Commits

Reviewing files that changed from the base of the PR and between bc03787 and 70fc2df.

⛔ Files ignored due to path filters (2)
  • assets/logo.png is excluded by !**/*.png
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (83)
  • .github/workflows/ci.yml
  • .gitignore
  • LEARNING_GUIDE/CHEAT_SHEET.md
  • LEARNING_GUIDE/CUMULATIVE_QUALITY_REPORT.md
  • LEARNING_GUIDE/README.md
  • LEARNING_GUIDE/VISUAL_GUIDE.md
  • LICENSE
  • NOTICE.md
  • PKG-INFO
  • README.md
  • benchmarks/adaptation_quality_report.py
  • benchmarks/cumulative_quality_report.py
  • benchmarks/integration_test_20ng.py
  • graphweave/__init__.py
  • graphweave/adaptation/__init__.py
  • graphweave/adaptation/_compat.py
  • graphweave/adaptation/adapter.py
  • graphweave/adaptation/config.py
  • graphweave/adaptation/correction.py
  • graphweave/adaptation/evaluation.py
  • graphweave/adaptation/keyphrase.py
  • graphweave/adaptation/pipeline.py
  • graphweave/adaptation/triplets.py
  • graphweave/core/__init__.py
  • graphweave/core/clustering.py
  • graphweave/core/embeddings.py
  • graphweave/core/graph_builder.py
  • graphweave/core/hierarchy.py
  • graphweave/core/keywords.py
  • graphweave/core/model.py
  • graphweave/cumulative/README.md
  • graphweave/cumulative/__init__.py
  • graphweave/cumulative/alignment.py
  • graphweave/cumulative/cumulative.py
  • graphweave/cumulative/datasets.py
  • graphweave/cumulative/evaluation.py
  • graphweave/cumulative/strategies.py
  • graphweave/labeling/__init__.py
  • graphweave/labeling/llm_granularity.py
  • graphweave/labeling/llm_labeler.py
  • graphweave/labeling/llm_merger.py
  • graphweave/utils/__init__.py
  • graphweave/utils/gpu.py
  • graphweave/utils/metrics.py
  • graphweave/utils/quote_verification.py
  • graphweave/utils/stopwords.py
  • graphweave/utils/timing.py
  • graphweave/visualization/__init__.py
  • graphweave/visualization/plotter.py
  • notebooks/challenges_clustering_kaggle.ipynb
  • notebooks/coreset_shorttext_test.ipynb
  • notebooks/cumulative_graphweave_benchmark.ipynb
  • notebooks/embedding_adaptation_demo.ipynb
  • notebooks/embedding_adaptation_kaggle.ipynb
  • notebooks/graphweave_full_demo.ipynb
  • pyproject.toml
  • run_benchmark.py
  • tests/conftest.py
  • tests/test_adaptation_correction.py
  • tests/test_adaptation_evaluation.py
  • tests/test_adaptation_keyphrase.py
  • tests/test_adaptation_linear.py
  • tests/test_adaptation_pipeline.py
  • tests/test_adaptation_triplets.py
  • tests/test_config.py
  • tests/test_coreset_improvements.py
  • tests/test_cumulative.py
  • tests/test_cumulative_llm_align.py
  • tests/test_cumulative_realistic.py
  • tests/test_hierarchy.py
  • tests/test_integration_20ng.py
  • tests/test_llm_granularity.py
  • tests/test_llm_labeler_reasoning.py
  • tests/test_model_basic.py
  • tests/test_quote_verification.py
  • tests/test_report_themes.py
  • tests/test_save_load.py
  • tests/test_stopwords.py
  • tritopic/__init__.py
  • tritopic/core/__init__.py
  • tritopic/labeling/__init__.py
  • tritopic/utils/__init__.py
  • tritopic/visualization/__init__.py

Comment thread .github/workflows/ci.yml
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.12"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test matrix omits declared-supported Python 3.11 and 3.13.

pyproject/PKG-INFO classifiers and the README advertise support for 3.9–3.13, but CI only exercises 3.9, 3.10, and 3.12. Compat regressions on 3.11/3.13 would ship undetected.

Suggested fix
-        python-version: ["3.9", "3.10", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
python-version: ["3.9", "3.10", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 24, Update the Python version matrix in the
CI workflow to include the declared-supported versions 3.11 and 3.13, while
preserving the existing 3.9, 3.10, and 3.12 entries so CI covers the full
supported 3.9–3.13 range.

Comment thread graphweave/core/model.py
Comment on lines +348 to +369

def _effective_min_cluster_size(self, n_docs: int) -> int:
"""Return the min_cluster_size to use for a corpus of n_docs documents.

If ``min_cluster_fraction`` is set, scales with corpus size so the
threshold is proportional rather than absolute — critical for cumulative
models whose fitting corpus grows from one batch to the full accumulator.
The absolute ``min_cluster_size`` acts as the floor.

With a weighted coreset (``sample_weights_``) the fraction scales with
the total *represented* mass rather than the coreset point count, so the
threshold lands in the same units as the summed cluster mass that
:meth:`ConsensusLeiden._handle_small_clusters` prunes against.
"""
if self.config.min_cluster_fraction is not None:
weights = getattr(self, "sample_weights_", None)
basis = float(weights.sum()) if weights is not None else n_docs
return max(
self.config.min_cluster_size,
int(self.config.min_cluster_fraction * basis),
)
return self.config.min_cluster_size

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Weighted min_cluster_fraction basis ignores the n_docs argument, breaking divide() for weighted (coreset) models.

When min_cluster_fraction is set and sample_weights_ exists, basis is always self.sample_weights_.sum() — the entire fitted corpus's weight mass — regardless of the n_docs passed in. This happens to be correct for _fit_single_pass/_fit_iterative/_auto_resolve_topic_count, which always call it with len(self.embeddings_) (the full corpus). But divide() (around line 1348) calls self._effective_min_cluster_size(subgraph.vcount()) where subgraph.vcount() is a topic subset size — the returned threshold still reflects whole-corpus weight mass, not the subgraph's. This is compounded by divide()'s sub_clusterer.fit_predict(...) never passing node_weights=, so even a correctly-scoped weighted threshold would be measured against unweighted document counts inside clustering. Net effect: dividing a topic on a weighted/coreset-fit model can silently apply a wildly wrong min_cluster_size, pruning sub-topics incorrectly.

🐛 Proposed fix — scope the weighted basis to the actual documents in question
-    def _effective_min_cluster_size(self, n_docs: int) -> int:
+    def _effective_min_cluster_size(self, n_docs: int, weights: np.ndarray | None = None) -> int:
         if self.config.min_cluster_fraction is not None:
-            weights = getattr(self, "sample_weights_", None)
+            if weights is None:
+                weights = getattr(self, "sample_weights_", None)
             basis = float(weights.sum()) if weights is not None else n_docs
             return max(
                 self.config.min_cluster_size,
                 int(self.config.min_cluster_fraction * basis),
             )
         return self.config.min_cluster_size

And in divide():

+        sub_weights = (
+            self.sample_weights_[doc_indices] if self.sample_weights_ is not None else None
+        )
         sub_labels = sub_clusterer.fit_predict(
-            subgraph, min_cluster_size=max(2, self._effective_min_cluster_size(subgraph.vcount()) // 2),
+            subgraph,
+            min_cluster_size=max(2, self._effective_min_cluster_size(subgraph.vcount(), sub_weights) // 2),
             resolution=best_res,
+            node_weights=sub_weights,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _effective_min_cluster_size(self, n_docs: int) -> int:
"""Return the min_cluster_size to use for a corpus of n_docs documents.
If ``min_cluster_fraction`` is set, scales with corpus size so the
threshold is proportional rather than absolutecritical for cumulative
models whose fitting corpus grows from one batch to the full accumulator.
The absolute ``min_cluster_size`` acts as the floor.
With a weighted coreset (``sample_weights_``) the fraction scales with
the total *represented* mass rather than the coreset point count, so the
threshold lands in the same units as the summed cluster mass that
:meth:`ConsensusLeiden._handle_small_clusters` prunes against.
"""
if self.config.min_cluster_fraction is not None:
weights = getattr(self, "sample_weights_", None)
basis = float(weights.sum()) if weights is not None else n_docs
return max(
self.config.min_cluster_size,
int(self.config.min_cluster_fraction * basis),
)
return self.config.min_cluster_size
def _effective_min_cluster_size(self, n_docs: int, weights: np.ndarray | None = None) -> int:
"""Return the min_cluster_size to use for a corpus of n_docs documents.
If ``min_cluster_fraction`` is set, scales with corpus size so the
threshold is proportional rather than absolutecritical for cumulative
models whose fitting corpus grows from one batch to the full accumulator.
The absolute ``min_cluster_size`` acts as the floor.
With a weighted coreset (``sample_weights_``) the fraction scales with
the total *represented* mass rather than the coreset point count, so the
threshold lands in the same units as the summed cluster mass that
:meth:`ConsensusLeiden._handle_small_clusters` prunes against.
"""
if self.config.min_cluster_fraction is not None:
if weights is None:
weights = getattr(self, "sample_weights_", None)
basis = float(weights.sum()) if weights is not None else n_docs
return max(
self.config.min_cluster_size,
int(self.config.min_cluster_fraction * basis),
)
return self.config.min_cluster_size
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/core/model.py` around lines 348 - 369, Update
_effective_min_cluster_size and divide so weighted thresholds use only the
documents represented by the current subgraph, not the full sample_weights_
total. In divide, derive the subgraph’s corresponding weights, use their summed
mass when calculating the threshold, and pass those same node weights to
sub_clusterer.fit_predict so pruning uses consistent weighted units; preserve
existing full-corpus behavior for other callers.

Comment thread graphweave/core/model.py
Comment on lines +806 to +814
f"({self.config.dim_reduction_method.upper()})...")

if self.config.dim_reduction_method == "umap":
from umap import UMAP
try:
from cuml.manifold import UMAP
if self.config.verbose:
print(" > Using cuML UMAP (GPU)")
except ImportError:
from umap import UMAP

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate model.py =="
fd -a 'model.py$' . | sed 's#^\./##'

echo "== relevant lines =="
if [ -f graphweave/core/model.py ]; then
  sed -n '760,860p' graphweave/core/model.py | cat -n
else
  f="$(fd 'model.py$' . | head -n 1)"
  sed -n '760,860p' "$f" | cat -n
fi

echo "== search dim_reduction config and docs mentions =="
rg -n "dim_reduction_method|dim reduction|UMAP|random_state|reproducible|reproducibility|byte" . -S

echo "== package requirements references =="
fd 'pyproject.toml|requirements.*|setup.py|environment.*|Dockerfile|docker-compose.yml' . -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}

Repository: nevil-mathew/GraphWeave

Length of output: 2038


🌐 Web query:

RAPIDS cuML UMAP random_state reproducibility memory optimizer sampling documentation

💡 Result:

In RAPIDS cuML, the random_state parameter in the UMAP estimator is used to control the random number generator for embedding initialization and optimizer sampling [1][2]. Setting this parameter enables reproducible embeddings, though it comes at the cost of slower training and increased memory usage due to reduced parallelism, which is otherwise used to optimize performance [1][2][3]. Key Considerations for Reproducibility: - Determinism: While setting random_state enables reproducible results, exact determinism is highly dependent on the algorithm and scale. Because high-parallelism GPU kernels often involve non-deterministic floating-point addition ordering, perfect bitwise reproducibility may not be guaranteed on all hardware or for all dataset sizes [1][2][4]. - Limitations: Setting build_algo='nn_descent' will break reproducibility because the NN-Descent algorithm produces non-deterministic K-Nearest Neighbor (KNN) graphs [1][2]. For strict reproducibility, you should avoid this algorithm or use precomputed KNN graphs [4]. - Initialization: Historically, some initialization methods (like spectral initialization) did not always honor the random_state. Recent updates have addressed this to ensure consistent UMAP embeddings [5]. Memory Optimization and Sampling: - Batched NN-Descent: To handle large datasets that do not fit in GPU memory, you can use batched NN-Descent [6]. By setting build_kwds={'n_clusters': N} (where N > 1), you enable batched generation of the KNN graph [7][6]. Increasing the number of clusters reduces peak GPU memory usage at the cost of performance, but it allows for processing much larger datasets [7][6]. - Managed Memory: You can further optimize memory usage by enabling managed memory via the RAPIDS Memory Manager (RMM), which allows UMAP to utilize system memory when GPU capacity is exceeded [7]. - Data Handling: For large inputs, ensure your data is efficiently handled. You can load data on the host (e.g., using mmap_mode='r' with NumPy) and pass data_on_host=True to the fit method to reduce initial device memory pressure [7]. Additionally, recent efforts have focused on reducing peak memory usage by releasing temporary arrays as soon as they are no longer needed during the fit process [8][9].

Citations:


🌐 Web query:

cuml.manifold UMAP import error if cuml unavailable umap import fallback

💡 Result:

The library cuml does not automatically fall back to the CPU-based umap-learn library if a standard from cuml.manifold import UMAP import fails due to missing dependencies or environment issues [1][2][3]. If the import from cuml.manifold import UMAP fails, it typically raises an ImportError, ModuleNotFoundError, or an OSError (e.g., related to missing CUDA drivers or incompatible versions) [1][2][3]. The cuml package is designed to be a GPU-accelerated implementation and requires a functional CUDA environment [3][4]. However, there is a distinct mechanism called cuml.accel that allows for GPU acceleration of third-party libraries, including the original umap-learn [5]. 1. cuml.accel Mechanism: If you have a workflow using the standard umap.UMAP (from umap-learn), you can enable the cuml.accel environment to intercept those calls and attempt to dispatch them to the GPU-accelerated cuml implementation [5]. 2. Automatic CPU Fallback: Within the cuml.accel framework, if a specific configuration (e.g., certain metrics or parameters like densmap=True) is not supported on the GPU, cuml will automatically fall back to the CPU-based umap-learn implementation [6][5]. This behavior is silent by default but can be monitored by setting CUML_ACCEL_LOG_LEVEL=info to see "falling back to CPU" messages [5]. 3. Troubleshooting Import Errors: If your import fails, it is usually an environment configuration issue rather than a missing fallback feature. Common causes include: - Incompatible CUDA driver or version mismatches (e.g., trying to use CUDA 12 packages with CUDA 11 drivers) [3]. - Missing required system libraries (e.g., libcudart.so) [3]. - Incompatible versions of cudf, pyarrow, or numba [1][7][6]. If you intend to use UMAP in environments where GPU availability is uncertain, the standard practice is to implement a manual try-except block in your code to switch between the two libraries: try: from cuml.manifold import UMAP except ImportError: from umap import UMAP This ensures your code remains functional regardless of whether cuml is installed or configured correctly [1][2].

Citations:


🌐 Web query:

cuml manufold UMAP random_state reproducibility issues GitHub 5147 5892

💡 Result:

In the cuML library, reproducibility issues related to the UMAP random_state parameter have been documented and addressed through multiple GitHub issues and pull requests, including those mentioned (Issues #5147 and #5892) [1][2]. Key findings regarding UMAP reproducibility in cuML: 1. Documentation and Precision: Historically, cuML's UMAP documentation incorrectly described random_state as providing only "3 decimal point match" for reproducibility [1]. Recent updates, such as PR #7518, have clarified that random_state is now intended to provide exact consistency, resolving the documentation discrepancies identified in Issue #5147 [1][3]. 2. Consistency Challenges: Issue #5892 highlighted cases where UMAP results could not be replicated even with a fixed random_state [2]. Investigations revealed that certain initialization methods—specifically spectral initialization—were not consistently honoring the random seed in earlier versions [2]. This was addressed by ensuring spectral initialization accepts a seed parameter [2]. 3. Known Limitations and Trade-offs: - Performance Cost: Setting a random_state enables reproducible embeddings but incurs performance costs, including slower training and increased memory usage, because high-parallelism optimizations (which are non-deterministic due to the non-associativity of floating-point addition) must be restricted [4]. - Algorithm Selection: To maintain determinism, cuML automatically switches to build_algo='brute_force_knn' when a random_state is provided, as the default nn_descent algorithm is non-deterministic [4][5]. - Scale: While random_state typically provides exact results for smaller datasets, reproducibility at very large scales remains a complex challenge due to floating-point behavior [1]. 4. Ongoing Maintenance: cuML developers frequently address stability and noise issues associated with seeded UMAP runs (e.g., Issues #6696 and #7176) [5][6]. Users experiencing instability or noise when using a fixed random_state are often advised to verify their init method (e.g., trying init='random' to bypass spectral embedding issues) and ensure they are using the latest version of cuML, as fixes for these behaviors are iteratively merged [2][7][6].

Citations:


Avoid silent UMAP backend autodetection for reproducible embeddings.

This block can pick cuml.manifold.UMAP or umap.UMAP depending only on imports, so the same random_state/config can produce different embeddings between GPU/RAPIDS and CPU-only environments, and cuML’s reproducibility trade-offs aren’t surfaced anywhere in the docs or CLI. Document this caveat, or gate the cuML path behind an explicit opt-in only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/core/model.py` around lines 806 - 814, Update the UMAP backend
selection in the dimensionality-reduction flow to require an explicit
configuration or CLI opt-in before importing and using cuML; otherwise always
use the CPU umap backend. Add the corresponding configuration/CLI documentation
describing backend selection and cuML reproducibility caveats, while preserving
the existing random-state behavior.

Comment on lines 261 to 266
if task == "classification":
# Cross-validated F1
# Cross-validated F1 (cv bounded by the rarest class so small classes don't crash it)
clf = LogisticRegression(max_iter=1000, random_state=42)
scores = cross_val_score(clf, features, y_true, cv=5, scoring="f1_macro")
cv = min(5, min(Counter(y_true).values()))
scores = cross_val_score(clf, features, y_true, cv=cv, scoring="f1_macro")
return float(np.mean(scores))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

sklearn cross_val_score StratifiedKFold minimum n_splits requirement

💡 Result:

In scikit-learn, the StratifiedKFold class requires the n_splits parameter to be at least 2 [1][2][3]. This constraint applies whether you instantiate StratifiedKFold directly or pass an integer to the cv parameter of cross_val_score (in which case scikit-learn internally uses StratifiedKFold for classifiers with binary or multiclass targets) [1][4][5]. Setting n_splits to a value less than 2 will raise a ValueError [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)graphweave/utils/metrics\.py$|metrics\.py$' || true

echo "== relevant source =="
if [ -f graphweave/utils/metrics.py ]; then
  nl -ba graphweave/utils/metrics.py | sed -n '230,280p'
fi

echo "== test behavioral constraints from installed sklearn if available =="
python3 - <<'PY'
import platform, sys
print("python", platform.python_version(), sys.version)
try:
    import sklearn
    print("sklearn", sklearn.__version__)
except Exception as e:
    print("sklearn_import_error", repr(e))
    raise SystemExit(0)

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from collections import Counter

y_true = np.array([0, 1, 2, 2])
counts = Counter(y_true).values()
print("min_count", min(counts))
cv = min(5, min(counts))
print("cv", cv)
try:
    scores = cross_val_score(LogisticRegression(max_iter=1000, random_state=42), np.arange(len(y_true)).reshape(-1,1), y_true, cv=cv, scoring="f1_macro")
    print("score_mean", float(np.mean(scores)))
except Exception as exc:
    print(type(exc).__name__, str(exc))
PY

Repository: nevil-mathew/GraphWeave

Length of output: 2038


🌐 Web query:

scikit-learn cross_val_score integer cv classifier uses StratifiedKFold n_splits must >= 2 min classes

💡 Result:

In scikit-learn, when the cv parameter in cross_val_score is an integer (or None), the library automatically selects the cross-validation strategy [1][2][3]. Specifically, if the estimator is a classifier (i.e., it derives from ClassifierMixin) and the target y is binary or multiclass, scikit-learn uses StratifiedKFold [4][1][3]. In all other cases, it uses KFold [1][3]. Regarding StratifiedKFold requirements, the n_splits parameter must be at least 2 [5]. Additionally, because StratifiedKFold attempts to preserve the percentage of samples for each class, each class must appear in the dataset at least as many times as there are folds (n_splits). If a class has fewer samples than n_splits, the stratification cannot be performed as intended, which typically results in an error during the cross-validation process.

Citations:


Guard the degenerate classification cross-validation case.

When the rarest class has only one sample, cv becomes 1, and scikit-learn raises ValueError because cross_val_score uses StratifiedKFold for classifiers, which requires n_splits >= 2. Handle this path explicitly instead of letting classification scoring crash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graphweave/utils/metrics.py` around lines 261 - 266, Update the
classification branch around the cv calculation and cross_val_score call to
handle a rarest-class count below two explicitly, avoiding cross-validation with
cv=1 and returning the appropriate degenerate-case score. Preserve the existing
cross-validated F1 behavior when cv is at least 2.

"metadata": {},
"outputs": [],
"source": [
"%pip install -q \"graphweave[llm,fast-knn] @ git+https://github.com/nevil-mathew/topic-extraction-poc.git@batch-clustering\"\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the referenced repo/branch resolves
git ls-remote https://github.com/nevil-mathew/topic-extraction-poc.git batch-clustering
echo "---"
git ls-remote https://github.com/nevil-mathew/GraphWeave.git

Repository: nevil-mathew/GraphWeave

Length of output: 2067


🌐 Web query:

GitHub repo nevil-mathew topic-extraction-poc branch batch-clustering

💡 Result:

There is no publicly available GitHub repository named "topic-extraction-poc" owned by "nevil-mathew" as of July 24, 2026 [1][2]. While Nevil Mathew is an active developer on GitHub with several public repositories (such as "ai-experiments", "awesome-readme", and "mentoring"), "topic-extraction-poc" is not listed among them [1][2]. Additionally, search results for this specific repository and branch combination yield no matches, suggesting the repository may be private, deleted, or misidentified [1][2]. Nevil Mathew is known for his work on projects like the ELEVATE-Project, where he has contributed to backend systems, cache handling, and CI/CD pipelines [3][4][5]. Research into topic extraction and batch clustering on GitHub frequently points to other projects such as "topicgen", "GitModel", or general "Topic-Modeling" repositories, but none are associated with this user [6][7][8].

Citations:


🌐 Web query:

GitHub repo nevil-mathew GraphWeave branch knn-to-ann

💡 Result:

There is no authoritative evidence of a branch named "knn-to-ann" within the GitHub repository "nevil-mathew/GraphWeave". The repository "nevil-mathew/GraphWeave" is associated with a project focused on multi-view graph topic modeling [1]. Research into its repository structure and online activity does not surface any branch by that name, nor does the project's documentation or related scholarly work (such as the research on graph generation via random walk trajectories) reference a transition or component specifically titled "knn-to-ann" [2][3][4]. It is possible that the query refers to a private branch, a misremembered name, or confusion with other software projects that utilize similar terminology (e.g., libraries for approximate nearest neighbor search or graph-based learning) [5][6][7].

Citations:


Fix the install source for the notebook.

This pip install points to nevil-mathew/topic-extraction-poc@batch-clustering, which does not resolve as a public repository/branch. Update this cell to install from the intended GraphWeave source and branch directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/challenges_clustering_kaggle.ipynb` at line 44, Update the
notebook’s pip install cell to use the intended public GraphWeave repository and
branch directly, replacing the unresolved topic-extraction-poc source while
preserving the graphweave extras and quiet installation options.

"id": "90235dcf",
"metadata": {},
"outputs": [],
"source": "# Uncomment on a fresh environment (e.g. Kaggle/Colab).\n# %pip install -q datasets sentence-transformers plotly scikit-learn\n!pip install \"graphweave[full] @ git+https://github.com/nevil-mathew/GraphWeave.git@rework\" --quiet 2>&1 | grep -E \"^(Successfully|ERROR:|Collecting|error)\" | head -20"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the GitHub repository nevil-mathew/GraphWeave have a branch named "rework"?

💡 Result:

There is no evidence that the GitHub repository nevil-mathew/GraphWeave has a branch named rework. Public information for this repository [1] indicates it contains multiple branches, but none of the available search results identify a branch with that name.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching notebook:"
git ls-files | grep -F 'notebooks/coreset_shorttext_test.ipynb' || true

echo
echo "Relevant install references:"
if [ -f notebooks/coreset_shorttext_test.ipynb ]; then
  rg -n "pip install|graphweave|rework|knn-to-ann|psutil|ReclusterContext|make_strategy" notebooks/coreset_shorttext_test.ipynb || true
fi

echo
echo "Repository references to rework/knn-to-ann:"
rg -n "nevil-mathew/GraphWeave|`@rework`|knn-to-ann|rework" . || true

echo
echo "Git remote/status info:"
git remote -v || true
git branch --show-current 2>/dev/null || true

Repository: nevil-mathew/GraphWeave

Length of output: 2038


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked notebook file:"
git ls-files | grep -F 'notebooks/coreset_shorttext_test.ipynb' || true

echo
echo "Relevant install references in notebook:"
if [ -f notebooks/coreset_shorttext_test.ipynb ]; then
  rg -n '"source"\s*:\s*".*pip install|graphweave|`@rework`|knn-to-ann|psutil|ReclusterContext|make_strategy' notebooks/coreset_shorttext_test.ipynb || true
fi

echo
echo "Repository references to rework/knn-to-ann/GraphWeave install:"
rg -n "nevil-mathew/GraphWeave|`@rework`|rework|knn-to-ann" . || true

echo
echo "Remotes/branch:"
git remote -v || true
git branch --show-current || true
git diff --stat || true

Repository: nevil-mathew/GraphWeave

Length of output: 2038


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked notebook file:"
git ls-files 'notebooks/coreset_shorttext_test.ipynb' || true

echo
echo "Relevant install references in notebook:"
if git ls-files 'notebooks/coreset_shorttext_test.ipynb' | grep -qx 'notebooks/coreset_shorttext_test.ipynb'; then
  rg -n '"source"\s*:\s*".*pip install|graphweave|`@rework`|knn-to-ann|psutil|ReclusterContext|make_strategy' notebooks/coreset_shorttext_test.ipynb || true
fi

echo
echo "Repository references to rework/knn-to-ann/GraphWeave install:"
rg -n "nevil-mathew/GraphWeave|`@rework`|rework|knn-to-ann" . || true

echo
echo "Remotes/branch:"
git remote -v || true
git branch --show-current || true
git diff --stat || true

Repository: nevil-mathew/GraphWeave

Length of output: 2038


Install from a stable commit, not the missing @rework branch.

graphweave[full] @ git+...@rework points to a branch that is not present on GitHub; re-running this notebook fails during install. Pin a release/tag or commit SHA instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/coreset_shorttext_test.ipynb` at line 42, Update the notebook’s
GraphWeave pip install command to replace the unavailable “rework” Git branch
with a stable release, tag, or commit SHA that exists in the repository. Keep
the existing extras, repository URL, quiet output, and filtering behavior
unchanged.

"# This notebook's current configuration doesn't use RAPIDS, so those conflicts\n",
"# have no effect here.\n",
"\n",
"!pip install \"graphweave[full] @ git+https://github.com/nevil-mathew/GraphWeave.git@rework\" --quiet 2>&1 | grep -E \"^(Successfully|ERROR:|Collecting|error)\" | head -20\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Install commands pin to transient branches / the old repo name and won't resolve post-merge. All four notebooks pip install GraphWeave from feature branches, and two still reference the pre-rebrand repo topic-extraction-poc. After this PR merges these installs will fetch the wrong (or non-existent) package. Point them at the GraphWeave repo on @main (or a released tag).

  • notebooks/cumulative_graphweave_benchmark.ipynb#L39: change GraphWeave.git@rework to @main/a tag.
  • notebooks/graphweave_full_demo.ipynb#L40: change GraphWeave.git@rework to @main/a tag.
  • notebooks/embedding_adaptation_demo.ipynb#L77: replace topic-extraction-poc.git@batch-clustering with the GraphWeave repo on @main/a tag.
  • notebooks/embedding_adaptation_kaggle.ipynb#L44: replace topic-extraction-poc.git@llm-embedding-adaptation with the GraphWeave repo on @main/a tag.
📍 Affects 4 files
  • notebooks/cumulative_graphweave_benchmark.ipynb#L39-L39 (this comment)
  • notebooks/graphweave_full_demo.ipynb#L40-L40
  • notebooks/embedding_adaptation_demo.ipynb#L77-L77
  • notebooks/embedding_adaptation_kaggle.ipynb#L44-L44
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notebooks/cumulative_graphweave_benchmark.ipynb` at line 39, Update all
notebook installation commands to use the GraphWeave repository on `@main` or a
released tag instead of transient branches. Change
notebooks/cumulative_graphweave_benchmark.ipynb:39 and
notebooks/graphweave_full_demo.ipynb:40 from GraphWeave.git@rework; replace the
old topic-extraction-poc references at
notebooks/embedding_adaptation_demo.ipynb:77 and
notebooks/embedding_adaptation_kaggle.ipynb:44 with the GraphWeave repository on
`@main` or a released tag.

Comment thread PKG-INFO
Comment on lines +8 to +10
Project-URL: Homepage, https://github.com/nevil-mathew/topic-extraction-poc
Project-URL: Repository, https://github.com/nevil-mathew/topic-extraction-poc
Project-URL: Issues, https://github.com/nevil-mathew/topic-extraction-poc/issues

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

PKG-INFO is stale and inconsistent with pyproject.toml.

This generated metadata still points at the old repo (topic-extraction-poc) whereas pyproject.toml/README.md use nevil-mathew/GraphWeave (also affects the CI badge at line 91). The embedded adaptation extra here (Lines 66-69) also lacks the sentence-transformers>=3.0 pin that pyproject.toml line 116 now adds. Since PKG-INFO is produced at build time, a committed copy will keep drifting from the source of truth — regenerate it from the current pyproject.toml, or drop it from version control entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PKG-INFO` around lines 8 - 10, Regenerate PKG-INFO from the current
pyproject.toml so its project URLs, CI badge metadata, and adaptation
extra—including sentence-transformers>=3.0—match the source configuration;
alternatively, remove PKG-INFO from version control to prevent future drift.

Comment on lines +78 to +90
"""
Drift scenario: 8 categories for the first 2 batches,
then 4 new categories appear in batch 3.

Established: categories 1-5 (comp.* topics)
Emerging: categories 11-14 (sci.crypt, sci.electronics, sci.med, sci.space)
These groups are vocabulary-distinct enough for LSA to detect drift cleanly.
Returns (batch_docs, batch_embs, batch_labels).
"""
# comp.* is vocabulary-technical; rec.sport.* is sports — very different in LSA space,
# giving ~22 % novelty and a clean drift signal even at 64-dim.
cats_old = [1, 2, 3, 4, 5] # comp.graphics, comp.os.*, comp.sys.*, comp.windows.*
cats_new = [9, 10] # rec.sport.baseball, rec.sport.hockey

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring/comments contradict the fixture's actual categories.

The docstring states "8 categories for the first 2 batches, then 4 new categories appear in batch 3" and "Emerging: categories 11-14 (sci.crypt, sci.electronics, sci.med, sci.space)", but the code uses cats_old = [1, 2, 3, 4, 5] and cats_new = [9, 10] (rec.sport.*). Align the prose with the code to avoid misleading maintainers about what drift this exercises.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_integration_20ng.py` around lines 78 - 90, Update the docstring
and nearby comments for the drift fixture to match cats_old = [1, 2, 3, 4, 5]
and cats_new = [9, 10]: describe five established comp.* categories followed by
two emerging rec.sport.* categories, and remove the incorrect sci.* and
eight/four-category claims.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants