File size: 13,931 Bytes
4b1014f 3956325 4b1014f 257f5bd 4b1014f 1873a1b 257f5bd 4b1014f 257f5bd 6710b04 257f5bd 6710b04 257f5bd 4b1014f 257f5bd 4b1014f 1dd7c3e b6b9678 4b1014f 257f5bd 4b1014f 50e4585 b6b9678 1dd7c3e 1b2a2fb 1dd7c3e b6b9678 1dd7c3e 50e4585 1b2a2fb 50e4585 1873a1b 50e4585 1dd7c3e 50e4585 1dd7c3e 4b1014f 257f5bd 4b1014f 1dd7c3e 257f5bd 1dd7c3e 257f5bd 1dd7c3e 257f5bd 1dd7c3e 1b2a2fb 257f5bd 1873a1b 1dd7c3e 1873a1b 1dd7c3e 1b2a2fb 257f5bd 1873a1b 257f5bd 4b1014f 257f5bd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | ---
license: apache-2.0
language:
- en
base_model: distilbert-base-uncased
pipeline_tag: text-classification
tags:
- topos
- scope-routing
- multi-label
- on-device
- privacy
datasets:
- AmazonScience/massive
- clinc/oos-eval
---
# Horos
**The boundary stone for personal data.** A 268 MB on-device router for
[Topos](https://github.com/dialoguesai/topos): free text in β the data scopes it touches,
an explicit `none`, or a hand-off to an LLM when it isn't sure.
**v2** (2026-08-16) β macro-F1 **0.613**, up from v1's 0.512. Trained against a gap v1's
own failures exposed: it handled *band* phrasings ("how has my sleep been") and fell
silent on *artifact* ones ("what's my resting heart rate trend"). Still not promoted β
see Limitations.

*α½
ΟΞΏΟ β the stones Athenians planted at property lines: "whose land begins here." Same
job, for your data. Horos runs before the permission gate, so the raw question never
leaves the device.*
**Router, not authorizer.** Every scope it emits is still permission-checked downstream.
A node without Horos escalates more often; it does not become less safe.
## Use
The `scope` role in a Topos model pack. Load through `topos.query.scope_head.load_head`
β it validates the label set against the live scope registry and the training manifest
against the licence policy before touching a weight. Not intended for any task besides
Topos scope routing, and not intended to run without the escalation path.
**Pin a revision.** The thresholds in `head.json` are selected per-artifact and the label
set can change; `main` is not a stable contract. Topos pins the SHA it was gated against:
```python
snapshot_download("Dialogues/horos", revision="1873a1b667dc2d1bd630835f1f4f47025f3864d9")
```
**The ladder.** Four branches, because "no scopes" is two different states β the model
deciding there is no personal data here, and the model having no idea:
```
labels β₯ tau_high, none low, nothing in band β ACT on the scope set (1..N scopes)
high none, nothing else β abstain: no personal data
any label in [tau_low, tau_high) β escalate to the pack's LLM (ambiguity)
nothing anywhere, none included β escalate to the pack's LLM (ignorance)
```
## Report card
Every release publishes all five blocks below, in full, **including the numbers that got
worse** β this is a standing contract, not a summary of a good run. Every row is versioned
and every cross-arm claim carries an interval. Each block earns its slot by having caught
something the others hid:
* **1 β the gate.** `disjoint` sat at 0.206β0.265 across three training rounds while
macro-F1 climbed 20%. An average would have called that pure progress.
* **2 β the split.** Artifact-concrete swallowing was 26% while the overall dead rate
looked healthy at 0.171. The total hid a defect the split names.
* **3 β unseen data.** `wrong` rose 11% β 17% in the same round routing improved 14
points. Nothing else here can see a model converting silence into error.
* **4 β composed.** Horos never runs alone; a head-only number is not a product number.
It is also where an unqualified 0.500-vs-0.495 claim went out on this card and turned
out to be inside the noise (see block 4).
* **5 β per-scope.** Every other block is an average or a count, and both let a gain on
one scope pay for a regression on another without saying so. Only this view names the
scope that got worse.
`swallowed` and `escalated` stay separate throughout: they have opposite fixes, and
collapsing them into "not answered" hides which one you have.
Blocks 1β3 and 5 regenerate from one command in `topos-eval`
(`scripts/scope_head_report_card.py --head A --label v1 --head B --label v2`). That
repo is private, so the numbers here are auditable by us and take-it-or-leave-it for
you β the benchmark itself is what we'd have to open to change that.
### 1. `role_classify_8` β the promotion gate
1,071 cases, template-disjoint from training, multi-label gold, 24% adversarial
negatives. Built to be hard: don't anchor against CLINC-style intent scores. The right
column is the same benchmark's LLM baseline.
| metric | v2 | v1 | mistral:7b (4.4 GB) | gate |
|---|---|---|---|---|
| macro-F1 | **0.613** | 0.512 | 0.495 | β₯ incumbent β
|
| exact set match | **0.558** | 0.496 | 0.243 | β |
| negatives abstained | 0.941 | 0.984 | 0.126 | β₯0.85 β
|
| single / multi-gold recall | 0.502 / 0.596 | 0.388 / 0.425 | β | gap β€0.05 β
|
| dead rate | **0.149** | 0.171 | β | <0.20 β
|
| **disjoint rate** | 0.206 | 0.217 | β | β€0.03 β |
| per-scope recall β₯ 0.60 | 6 / 14 | 2 / 14 | 9 / 14 | 14/14 β |
Which six, and which scope went backwards: block 5.
### 2. Real language β a gap finder, not a score
53 hand-annotated natural phrasings neither version trained on. The labels are one
annotator's judgement, so the totals measure agreement-with-the-annotator; the **split**
is the signal worth acting on.
| metric | v2 | v1 | gate |
|---|---|---|---|
| correct | **0.774** | 0.585 | β |
| swallowed | **0.132** | 0.189 | β |
| escalated | 0.094 | 0.189 | β |
| **swallowed β artifact-concrete** | 0.176 | 0.265 | β€0.10 β |
| swallowed β abstract / band | 0.053 | 0.053 | β |
### 3. Unseen phrasings β did the authoring generalise?
2,745 rows from the schema-grounded generator's heldout half, which the training corpus
never draws from. Provably unseen by both versions and absent from the benchmark. This
block separates *learning a register* from *memorising the rows we wrote*.
| metric | v2 | v1 | ratchet |
|---|---|---|---|
| routed | **0.565** | 0.421 | β₯ incumbent β
|
| swallowed | **0.172** | 0.214 | β€ incumbent β
|
| **wrong scope** | 0.169 | 0.113 | β€ incumbent β |
| escalated | 0.094 | 0.252 | β |
**Read the `wrong` row, not just the first one.** v2 routes 14 points more of this
traffic and is confidently wrong 6 points more often. It got better at recognising that
a question concerns personal data, and no better at discriminating *which* data β the
same finding as the flat `disjoint` rate. Those are separate abilities; one improved.
### 4. Composed with its LLM escalation
Horos is not deployed alone, so this is the number that describes the product. Each case
is routed by the ladder above: Horos answers when it acts or abstains, the LLM sees only
what it escalates.
| arm | macro-F1 | exact | neg-abstain | wrong-scope | LLM sees |
|---|---|---|---|---|---|
| mistral:7b (4.4 GB) alone | 0.495 | 0.243 | 0.126 | 0.237 | 100% |
| Horos **v1** + mistral:7b | 0.550 | 0.524 | 0.972 | 0.143 | 16.4% |
| **Horos v2 + mistral:7b** | **0.620** | **0.578** | 0.909 | 0.163 | **15.3%** |
| llama3.2 (2 GB) alone | 0.376 | 0.261 | 0.510 | 0.204 | 100% |
| Horos **v1** + llama3.2 | 0.500 | 0.508 | 0.976 | 0.146 | 16.4% |
| **Horos v2 + llama3.2** | **0.583** | **0.565** | 0.917 | 0.146 | **15.3%** |
**A 2 GB machine running the hybrid beats a 4.4 GB machine running LLM-only** β 0.583 vs
0.495, **+0.088, 95% CI [+0.052, +0.123]** (paired bootstrap, 2,000 resamples of the same
1,071 cases). Horos is identical on every hardware tier, so only the escalated ~1/6th of
traffic degrades with weaker hardware.
That claim did not survive this test at v1, where the same comparison was +0.005, CI
[β0.036, +0.042] β indistinguishable from zero. It was stated on this card as fact for
one release. Cross-arm claims here now carry an interval or they don't get made.
Note the direction of the v1 β v2 trade in this table: composed accuracy up ~7 points,
composed negatives-abstained down ~6. The escalation path was covering for the head's
false-positives, and v2 hands it less to cover.
### 5. Per-scope β where it moved, and where it didn't
Blocks 1 and 3 report counts ("6 / 14 above the floor", "routed 0.565"). A count cannot
be audited: it says how many scopes cleared the bar and never which, so a large gain on
one scope silently pays for a regression on another. Both slices, sorted by movement.
**Unseen phrasings** β did the gains reach language nobody wrote down?
| scope | n | v1 | v2 | Ξ |
|---|---|---|---|---|
| `places` | 217 | 30% | 68% | **+37** |
| `schedule` | 65 | 31% | 65% | **+34** |
| `work_context` | 420 | 22% | 51% | **+29** |
| `relationship_context` | 174 | 16% | 42% | **+26** |
| `public_bio` | 229 | 67% | 85% | +17 |
| `messages` | 253 | 49% | 66% | +17 |
| `attention` | 145 | 35% | 46% | +10 |
| `complexity` | 178 | 39% | 47% | +8 |
| `activity` | 308 | 61% | 68% | +7 |
| `resources` | 151 | 28% | 34% | +6 |
| `availability` | 116 | 21% | 25% | +4 |
| `contacts` | 119 | 76% | 73% | β3 |
| `ai_conversations` | 111 | 42% | 39% | β4 |
| `health` | 259 | 61% | 56% | **β5** |
**Gate benchmark** β this is what "6 / 14 above the floor" expands to.
| scope | n | v1 | v2 | Ξ | β₯0.60 |
|---|---|---|---|---|---|
| `contacts` | 58 | 69% | 83% | +14 | β
|
| `public_bio` | 54 | 67% | 81% | +15 | β
|
| `attention` | 73 | 25% | 71% | **+47** | β
|
| `schedule` | 59 | 44% | 69% | +25 | β
|
| `activity` | 78 | 58% | 64% | +6 | β
|
| `messages` | 61 | 51% | 64% | +13 | β
|
| `health` | 116 | 47% | 54% | +8 | β |
| `places` | 61 | 26% | 49% | +23 | β |
| `availability` | 62 | 35% | 45% | +10 | β |
| `resources` | 70 | 27% | 37% | +10 | β |
| `complexity` | 82 | 35% | 37% | +1 | β |
| `ai_conversations` | 54 | 43% | 35% | **β7** | β |
| `relationship_context` | 77 | 17% | 34% | +17 | β |
| `work_context` | 73 | 25% | 30% | +5 | β |
Three things only this view shows:
* **`ai_conversations` is down on both slices** (β7, β4) β the one unambiguous regression,
not a slice artifact. v2's training targeted artifact-concrete phrasings, and questions
about your own past AI conversations are the scope least like an artifact.
* **`health` and `contacts` flip sign between slices.** `health` gains 8 on the benchmark
and loses 5 on unseen phrasings; `contacts` gains 14 and loses 3. The two instruments
measure genuinely different things, and a card publishing only one of them would report
either as a clean win.
* **The four biggest unseen gains are exactly the four scopes v2's authoring targeted**
(`places`, `schedule`, `work_context`, `relationship_context`, +26 to +37). That is the
evidence the authoring generalised rather than being memorised β the gains land on
phrasings of those scopes that nobody wrote down.
Floors are still unmet: on the gate slice, 8 of 14 scopes sit under the 0.60 recall bar.
`work_context` (30%), `complexity` (37%) and `resources` (37%) are the weakest, and
`relationship_context` at 34% remains the hardest scope in the taxonomy.
## Limitations
* **Confident-none swallowing, now concentrated rather than general.** Overall dead rate
is 0.149, but on *artifact-concrete* phrasings ("what's my bank balance", "what's in
my review queue") it is **17.6%** against 5.3% on abstract ones. v2 closed most of v1's
26% gap here and gave part of it back for the safety property below β one threshold
currently trades them against each other, which is the next fix. Treat a `none` on a
plausibly-personal question with suspicion.
* **Disjoint rate 0.206 β the blocking defect, and unsolved.** One in five acted-on
turns names a scope set sharing nothing with the truth. It is **threshold-invariant**
(0.256β0.270 across Ο 0.4β0.9 on a sibling run), so it lives in the weights, not the
operating point. Three training rounds have not moved it.
* **v2 gained recognition, not discrimination.** It routes far more personal-data
questions than v1 and is confidently wrong on a larger share of them (block 3: `wrong`
0.113 β 0.169, against a flat `disjoint`). Knowing a question is about your data and
knowing *which* of your data are separate abilities; this round only advanced the
first. The escalation path absorbs less of that than it used to β composed
negatives-abstained fell 0.972 β 0.909.
* **8 of 14 scopes are under the 0.60 recall floor** (v1: 12), weakest `work_context`
30%, `relationship_context` 34%, `complexity` and `resources` 37%. This artifact has
not cleared its promotion gate; it fronts an LLM in shadow/advisory postures only.
* **`ai_conversations` regressed on both slices** (β7 gate, β4 unseen) β the one scope
v2 made unambiguously worse. Questions about your own past AI conversations are the
scope least like the artifact-concrete register v2 was trained to fix.
* All numbers are synthetic-benchmark. Real-traffic behaviour is being measured in
shadow mode.
* English only. **No user data, ever** β the loader refuses artifacts whose manifest
says otherwise.
## Training data
| source | licence | rows |
|---|---|---|
| schema-grounded synthetic + compounds + boundary negatives (Topos) | internal | 7,369 |
| [AmazonScience/massive](https://huggingface.co/datasets/AmazonScience/massive) | CC BY 4.0 | 5,200 |
| [CLINC150 OOS](https://github.com/clinc/oos-eval) | CC BY 3.0 | 1,200 |
v2 added ~95 artifact-concrete realizations **and** restored the concept-negative ratio
(0.054 β 0.070). The second half is the interesting one: without a "what *is* a
curriculum vitae?" negative beside "what's on *my* CV?", an earlier v2 candidate learned
the artifact noun as sufficient evidence of ownership and fired on 43 near-miss
definition questions, dropping negatives-abstained to 0.739. Teaching a positive without
its matching negative was the whole regression.
This card is the CC BY attribution notice. No share-alike or non-commercial data.
## Architecture
DistilBERT-base-uncased, 15 sigmoid outputs (14 scopes + `none`), `BCEWithLogitsLoss`
with per-label `pos_weight`, max_length 64. Artifact = `head.json` (labels, thresholds,
manifest, metrics) + `model/` (HF-standard).
|