Venn

Precedent Desk: your closed tickets are the model#

A support desk routes tickets to queues. Northwind Bank's card-ops desk has 10,003 closed tickets, each with the queue it was resolved in. This demo points Venn at those tickets and asks the same question a text-reading decision model answers, with nothing trained: which queue handles this ticket. The same lookup returns the precedents, flags a ticket that does not fit its queue, learns a new queue from one resolved ticket, and sweeps the whole backlog in one pass.

The desk is played by Banking77 (CC BY 4.0): the training split is the store of closed tickets, the test split is the incoming stream, and the 77 intents are the queues. It is the one public dataset with a published Jev number, so the comparison is on the same 3,080 rows. The resolution notes beside each precedent are authored illustrations, not data. This harness retains the official splits: seven test rows have normalized-text matches in training, all with the same label. The 13,083 rows contain 13,071 distinct normalized texts. Historical scores are not deduplicated or rescored; this overlap limits the held-out interpretation.

What was measured#

Every number on screen is read from results.json, written by eval.py on CPU from the published vectors. Accuracy is top-1 by queue over all 3,080 test tickets. Intervals cover test-row variation (1,000-row bootstrap) and which tickets the store kept (25 store draws), pooled, except for the held-out-queue intervals described below. The original protocol and thresholds were fixed before its run; later amendments and exploratory analyses are not all covered by that registration.

What Result
Jev 1.13, matched run on the 3,080 test tickets, one fixed prompt over the 77 queue names 79.7% [78.3, 81.1]
Venn, zero-shot: the 77 queue names only, no store 92.1% [91.1, 93.0]
Venn, nearest reference, 5 closed tickets per queue 91.2% [89.9, 92.4]
10 per queue 92.3% [91.2, 93.4]
20 per queue (full test split, not the VennSpace-1 cell) 92.8% [91.8, 93.8]
50 per queue 93.3% [92.3, 94.3]
All 10,003 closed tickets 93.6% [92.7, 94.4]
Fine-tuned classifiers (published, different model family) about 93.8%
Store with 10% / 20% / 30% of queue labels wrong 84.1% / 74.8% / 65.4%

Provenance of the numbers: the 92.8% that circulated before this demo is VennSpace-1's Banking77 nearest-reference row, a 20-references-per-queue, 20-queries-per-queue cell (1,540 queries). The full-split cells measured here are different numbers: 92.1% zero-shot, 93.6% with all closed tickets, 79.7% for Jev's matched run. This harness's 20-per-queue point estimate rounds to the same 92.8%, but uses 3,080 queries and pooled row/store-draw intervals, not VennSpace-1's 1,540 queries and intent-label bootstrap.

The crossover sentence the design asked for holds at the smallest stop tested: five resolved tickets per queue already clear Jev's interval. Jev saw none of the training rows; Venn's store was all of them. Same rows, different information.

The zero-shot row is not what it looks like. Two gates were run before any claim about it. Replacing the 77 original queue names with fresh one-line descriptions written without those names drops zero-shot from 92.1% to 69.0% [67.3, 70.6], below Jev; opaque codes give 0.7%, chance. On queues held out of the store, the original name alone reaches 67% of a held-out queue's tickets where a fresh description reaches 32%. The original label strings carry far more than their meaning, which is what memorized dataset labels look like, and Banking77 is a standard embedding benchmark. The number the demo treats as Venn's no-history baseline is therefore the description-based 69.0%; the 92.1% is shown in the app with this caveat and nowhere else.

A second dataset (later amendment; history curve exploratory). On CLINC150 (pinned clinc_oos "plus", 4,498 in-scope test rows, 150 intents), Jev's matched run scored 91.1% [90.2, 91.9] and Venn zero-shot over the 150 intent names 69.2% [67.8, 70.5]. The history curve replicates with a later crossover: 87.8 / 90.6 / 92.3 / 93.8 / 94.3% at 5 / 10 / 20 / 50 / all (100) training rows per intent, clearing Jev's interval from 50 per intent; the 11-vote variant reaches 94.6%. So the pattern that holds on these two measured datasets is "the desk's closed tickets outscore the decision model once there are enough of them", not "the queue names outscore it". Item k's history curve and 11-vote result were added after seeing item j; they are exploratory, not an independently pre-registered confirmation.

CLINC150, 4,498 test rows Accuracy
Jev 1.13, matched run 91.1% [90.2, 91.9]
Venn zero-shot, intent names only 69.2% [67.8, 70.5]
Nearest reference, 5 / 10 / 20 / 50 / all per intent 87.8 / 90.6 / 92.3 / 93.8 / 94.3%

Label noise and the vote. With 10 / 20 / 30% of store labels reassigned, the best-single-reference rule falls to 84.1 / 74.8 / 65.4%, below Jev from 20% on. A vote among the 11 nearest stored tickets holds at 93.7 / 93.7 / 93.6%, the same as with clean labels; the app's "Show the label-noise lines" checkbox shows both.

One new experience. Seven queues were held out of the store entirely (ten random choices of seven). A ticket from a held-out queue arrives with no precedent: the nearest-reference margin fell below the abstention threshold on 81% of first arrivals [71, 90] (threshold set for 90% coverage on known queues; the set-membership flag fired on 94%). Writing that one resolved ticket back routes the queue's other test tickets at 57% [51, 63]; two tickets 72%, three 77%, five 81%. The pre-registered gate for using the word "learning" (one-shot lower bound above 50%, abstention on at least 80% of first arrivals) passed. The counter never scores the ticket that was written.

That is the historical gate result under the registered cell bootstrap: 70 intent-within-choice cells (ten choices of seven), not ten choice clusters. Cells share stores and thresholds, and intents can recur across choices. The interval is descriptive of that cell resampling, not a cluster-aware generalization guarantee. Whether a choice-cluster or repeated-intent analysis changes the gate is unmeasured and deferred; thresholds and results are retained. The abstention gate uses the point rate, not its interval's lower bound.

Backlog sweep. One index_apply pass over all 13,083 ticket rows takes 0.34 s in process and 0.34 s over loopback HTTP including client parsing (index upload 6 s, timed separately), on 8 CPU cores. The pre-registered two-option question ("affected by the FX-fee change / not", threshold 0.9) is a poor sweep: precision 5%, recall 21%, 9,505 rows sent to review. Re-tagging the backlog with the 77 queue names and filing the two FX queues, added after that result and labelled exploratory, finds 361 tickets at 98.1% precision and 98.6% recall. The demo shows the measured row it was registered with and the exploratory row beside it.

Registration (app only). A family head fitted on 20 or 35 closed tickets per queue, 25 draws each, scored on the 3,080 test tickets against nearest reference over the same store. These rows were recorded when register_family split the examples 60/20/20 (train/selection/report) within each queue by default. It now trains on every example unless a split is given, so item d passes that 60/20/20 split explicitly. A one-draw check with the current code reproduced the report-slice sizes (308 and 539 items) and the calibrated flags; its fits took 323 s and 415 s on a shared machine. The fit times in the table are from the earlier run:

Examples per queue Head Nearest reference, same store Head − nearest calibrated Test ECE p ≥ 0.95 on the report slice Fit time (recorded)
20 92.3% 92.8% −0.5 [−0.6, −0.4] never (308 report items) 0.061 precision 100%, coverage 95.9% [95.1, 96.7] 95 s
35 92.4% 93.1% −0.7 [−0.8, −0.6] always (539 report items) 0.056 precision 100.0%, coverage 96.5% [95.9, 97.1] 153 s

No accuracy gain over nearest reference is claimed; the head is half a point behind it. The calibrated flag and the p ≥ 0.95 figures come from the service's own held-out report slice, which is drawn from the training split; on the test tickets the same head's ECE is 0.056 to 0.061, ten times the report-slice value, so an auto-action threshold read off the report slice is an estimate with an interval and a known gap, never a guarantee.

The wrong tool, on purpose. "I was NOT charged twice. One charge, but my statement shows two lines." Venn's precedents say duplicate charge; a text reader can read the negation. The closing rule: route by Venn when a precedent clears the margin threshold, fall through to Jev when it does not, write every resolution back.

Run it#

pip install -e ".[embedding,benchmark,dev]"
for f in banking77-vectors banking77-gate-vectors clinc-vectors clinc-train-vectors; do
  curl -LO "https://github.com/markrussinovich/venn-vdm/releases/download/media-v1/$f.npz"
done
python examples/precedent_desk/eval.py --vectors banking77-vectors.npz --output /tmp/precedent-desk \
    --model-dir "$(venn download-model)" --items abcfghijk --check-texts
python examples/precedent_desk/app.py --vectors banking77-vectors.npz \
    --results /tmp/precedent-desk/results.json --model-dir "$(venn download-model)"

Then open http://127.0.0.1:8765.

eval.py reads the gate and CLINC files from beside banking77-vectors.npz (or --gate-vectors, --clinc-vectors and --clinc-train-vectors). Item h needs banking77-gate-vectors.npz: it measures the 69.0% description baseline and the label-string gate that qualifies the 92.1% zero-shot figure. Items j and k need the two CLINC files. The app also reads banking77-gate-vectors.npz for its "fresh queue descriptions" option; without it, that option is disabled and the app routes with the original names. --items abcfghijk writes every number in the tables above except three: Jev's matched Banking77 run (79.7%), Jev's CLINC150 run (91.1%) and the registration table.

  • Jev, 79.7%: item e. It needs OPENROUTER_API_KEY and costs about $0.21 for the 3,080 requests, cached in <output>/jev-responses.jsonl. With no key, item e reads a complete cache from an earlier run and makes no requests; with neither, it writes the third-party 81.0% placeholder, and the app shows that 81.0% with its source instead of 79.7%. Items run with e first, so --items eabcfghijk uses the matched run throughout. Pass the cache to the app with --jev-cache so the Jev column shows the matched answers.
  • Jev on CLINC150, 91.1%: item j with OPENROUTER_API_KEY set; without a key, item j measures only Venn's side.
  • Registration: item d fits 50 family heads (two sizes, 25 draws) and scores each on the 3,080 test tickets. One draw of each size took 13 minutes in the check above, so the default 25 draws take several hours; --draws lowers the number of draws.

The items take a few minutes each on 8 CPU cores, except d. On a release-source checkout, the app fetches ticket texts from the original pinned Hugging Face revision and checks hashes and labels against the vectors before caching them in --state (default $VENN_HOME/precedent-desk, with VENN_HOME defaulting to ~/.cache/venn) as tickets.json; --tickets chooses another path. No upstream ticket texts are shipped in release source. Typing a ticket of your own needs the pinned reader locally (venn download-embedder).

The vectors file holds EmbeddingGemma 2 vectors (float16, venn-1 profile, Classification prompt) for the 13,083 Banking77 rows plus the option and instruction vectors the questions use. It carries Banking77's CC BY 4.0 attribution; it is an explicitly attributed derived-vector release exception to the no-dataset-text boundary, not permission to redistribute upstream text. The vectors file is not part of the source package.

New evaluation outputs keep the legacy Banking77 vectors_sha256 and also record consumed gate/CLINC vector files, available cached comparison inputs, head artifacts, source pins and implementation fingerprints. Item j verifies ordered CLINC hashes, labels and original row IDs against the pinned source. Item k checks both splits when --check-texts is enabled; otherwise it retains the local-vector-only CPU path and explicitly reports source-row verification as unchecked. These checks do not remeasure historical results.

What this does and does not claim#

Said: on the same 3,080 public rows, the desk's history scored 93.6% and Jev 79.7%, with the information difference stated; nothing was trained; five resolved tickets per queue already outscore the decision model; one resolved ticket of a new kind routes the next ones at 57%, measured on held-out rows; the same vector answers precedent, queue fit and the backlog sweep.

Not said: that Venn beats Jev at classification in general (JevBench's public slice is 55% against 82 to 84%); that any of this is zero-shot when the store is used; "learns" about a written ticket being retrieved; which handling path works (there is no outcome data; nearest outcome is not in this demo); that an auto-action threshold is proven or safe; any sweep time without its boundary. Banking77 rows may be in the reader's training data, as disclosed for VennSpace-1; labels are expert-assigned, so the public number is an upper bound on what a desk's own clicked queue field would give (the noise rows are the honest answer).

View this page on GitHub

On this page