5 Commits

Author SHA1 Message Date
KS Jannette
362a47f88a Upgraded embedding model to voyage-3.5, updated README.md 2026-08-01 05:35:06 -04:00
KS Jannette
c6b07ccb56 fix minor css issue on stickies 2026-08-01 04:50:40 -04:00
KS Jannette
8e79e78006 hotfix 2026-08-01 04:41:31 -04:00
19d4a82c90 Merge pull request 'Re-aligned heuristic, updated readme, added code comment' (#3) from BUG-cohesion-scoreUI-display into master
Reviewed-on: #3
2026-08-01 08:13:29 +00:00
KS Jannette
6f09b6ecdc Re-aligned heuristic, updated readme, added code comment 2026-08-01 04:06:39 -04:00
6 changed files with 193 additions and 174 deletions

View File

@@ -8,11 +8,15 @@ In kongruity, the artifacts become "sticky notes." A board full of them looks ch
With a click, they are semantically evaluated, grouped into thematic clusters with descriptive headers, rankable and exportable to project planning and execution tools. With a click, they are semantically evaluated, grouped into thematic clusters with descriptive headers, rankable and exportable to project planning and execution tools.
## Voyage AI voyage-3.5
![Embedding model benchmarking.](Voyage.jpg)
## Clustering and evaluation: methodology ## Clustering and evaluation: methodology
Two models run in parallel, and neither sees the other's work. Anthropic's `claude-sonnet-5` (`backend/services/clustering.service.js`) reads the raw text of every note and groups them into labeled thematic clusters. Two models run in parallel, and neither sees the other's work. Anthropic's `claude-sonnet-5` (`backend/services/clustering.service.js`) reads the raw text of every note and groups them into labeled thematic clusters.
At the same time, Voyage AI's voyage-3 model (`backend/services/embedding.service.js`) converts each note's text into a numeric representation of its semantic meaning aka vector. At the same time, Voyage AI's voyage-3.5 model (`backend/services/embedding.service.js`) converts each note's text into a numeric representation of its semantic meaning aka vector.
Once the LLM returns, kongruity scores that grouping (`backend/services/validation.service.js`) using an established silhouette coefficient, with cosine distance rather than Euclidean as the proximity metric. Once the LLM returns, kongruity scores that grouping (`backend/services/validation.service.js`) using an established silhouette coefficient, with cosine distance rather than Euclidean as the proximity metric.
@@ -30,14 +34,18 @@ Average silhouette width is a widely-used measure of clustering quality. Higher
2. How well-separated each cluster is from its nearest neighboring cluster. 2. How well-separated each cluster is from its nearest neighboring cluster.
Although the coefficient is mathematically bounded by [−1, 1], cosine distance between high-dimensional text embeddings is compressed: unrelated notes sit close to orthogonal, so both the within-cluster and nearest-cluster distances land near 0.8. Because silhouette divides the gap between them by the larger of the two, the practical range on embedding data is roughly [−0.05, 0.10] rather than the full interval.
The bands below are therefore calibrated against that observed range. On the seed board, the five ideal thematic clusters score 0.09; swapping a few notes between clusters drops it to 0.06; a scrambled assignment falls below zero.
The score appears above the results with a plain-language band: The score appears above the results with a plain-language band:
- **0.70 and above** — Strong - **0.07 and above** — Strong
- **0.40 to 0.69** — Moderate - **0.04 to 0.06** — Moderate
- **0.10 to 0.39** — Weak - **0.01 to 0.03** — Weak
- **Below 0.10** — Poor - **Below 0.01** — Poor
Silhouette values are archetypically bounded below 1.0 for real-world data, so the number is best read as a relative measure. See Hugo Sträng, Tai Dinh. An upper bound on the silhouette evaluation metric for clustering. Pattern Recognition, Volume 178, 2026, 113402, ISSN 0031-3203. A score near 0.00 means the grouping is no better than chance. Bands are specific to `voyage-3` cosine distance and would need recalibration behind a different embedding model. See Hugo Sträng, Tai Dinh. An upper bound on the silhouette evaluation metric for clustering. Pattern Recognition, Volume 178, 2026, 113402, ISSN 0031-3203.
## Organizing clusters, exporting to workflow software ## Organizing clusters, exporting to workflow software

BIN
Voyage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -27,7 +27,7 @@ export const embedNotes = async (notes) => {
for await (const chunk of batches) { for await (const chunk of batches) {
const response = await client.embed({ const response = await client.embed({
input: chunk.map((n) => n.text), input: chunk.map((n) => n.text),
model: "voyage-3", model: "voyage-3.5",
}); });
response.data.forEach((item, i) => { response.data.forEach((item, i) => {

View File

@@ -6,10 +6,14 @@ import Sticky from './sticky';
import Button from './button'; import Button from './button';
import '../styles/stickies.css'; import '../styles/stickies.css';
// In practice, silhouette on cosine distance between text embeddings occupies roughly
// [-0.05, 0.10], not strict theoretical [-1, 1]: near-orthogonal vectors put both the within- and
// nearest-cluster distances close to 0.8, and the coefficient divides their gap
// by the larger. These bands are calibrated to that range for voyage-3. see README, Reading the cohesion score
const scoreLabel = (score: number): string => { const scoreLabel = (score: number): string => {
if (score >= 0.7) return 'Strong'; if (score >= 0.07) return 'Strong';
if (score >= 0.4) return 'Moderate'; if (score >= 0.04) return 'Moderate';
if (score >= 0.1) return 'Weak'; if (score >= 0.01) return 'Weak';
return 'Poor'; return 'Poor';
}; };
@@ -83,7 +87,7 @@ const Stickies = () => {
dragIndex.current = null; dragIndex.current = null;
setDragOverIndex(null); setDragOverIndex(null);
}; };
console.log(score?.toFixed(2))
return ( return (
<div className="stickies-container"> <div className="stickies-container">
<Button onClick={handleCluster} isLoading={isPending} label="Group Stickies By Topic" /> <Button onClick={handleCluster} isLoading={isPending} label="Group Stickies By Topic" />
@@ -91,7 +95,7 @@ const Stickies = () => {
<div className="clusters-container"> <div className="clusters-container">
{score != null && ( {score != null && (
<div className="cohesion-score"> <div className="cohesion-score">
Cluster cohesion: <strong>{score.toFixed(2)}</strong> — {scoreLabel(score)} Cluster cohesion: <strong>{scoreLabel(score)}</strong>
</div> </div>
)} )}
{rankedClusters.map((group, index) => ( {rankedClusters.map((group, index) => (

View File

@@ -40,10 +40,12 @@
} }
.cluster-header { .cluster-header {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
min-height: 32px;
} }
.cluster-rank { .cluster-rank {
@@ -69,13 +71,18 @@
} }
.cluster-label { .cluster-label {
position: absolute;
left: 50%;
transform: translateX(-50%);
max-width: 50%;
margin: 0; margin: 0;
font-size: 1.2em; font-size: 1.2em;
font-weight: 600; font-weight: 600;
flex: 1; pointer-events: none;
} }
.cluster-drag-handle { .cluster-drag-handle {
margin-left: auto;
font-size: 1.4em; font-size: 1.4em;
color: #6dd6f4; color: #6dd6f4;
opacity: 0.4; opacity: 0.4;

View File

@@ -14,7 +14,7 @@ const MOCK_CLUSTER_RESPONSE = {
{ label: 'Auth Issues', noteIds: ['note_001'] }, { label: 'Auth Issues', noteIds: ['note_001'] },
{ label: 'Export Issues', noteIds: ['note_002'] }, { label: 'Export Issues', noteIds: ['note_002'] },
], ],
score: 0.74, score: 0.09,
}; };
let fetchMock: ReturnType<typeof vi.fn>; let fetchMock: ReturnType<typeof vi.fn>;