Zero-Shot Classification
Transformers
Safetensors
GGUF
English
decision-model
system-one
falcondec
lightdec
calibrated-decisions
multiple-choice
intent-classification
customer-support
natural-language-inference
code
guardrails
agents
selective-prediction
falconsai
model-surgeon
attested-lineage
Instructions to use Falconsai/LightDec with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Falconsai/LightDec with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("zero-shot-classification", model="Falconsai/LightDec")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Falconsai/LightDec", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Update README.md
Browse files
README.md
CHANGED
|
@@ -1,117 +1,969 @@
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
-
language:
|
|
|
|
| 4 |
library_name: transformers
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
---
|
| 7 |
-
# FalconDec v1.0.0 — Falcon Decision
|
| 8 |
|
| 9 |
-
|
| 10 |
-
[`Falconsai/proof_v2`](https://huggingface.co/Falconsai/proof_v2).
|
| 11 |
|
| 12 |
-
*
|
| 13 |
-
* Parameters: 159.7M · weights: fp16 305 MB, int8 153 MB
|
| 14 |
-
* Layout: `[CLS] question [SEP] [MASK] opt1 … [MASK] optk [SEP] state [SEP]`; set-transformer option head;
|
| 15 |
-
temperature per (question type × option-count bucket).
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
```python
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
from huggingface_hub import snapshot_download
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
model,
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
library_name: transformers
|
| 6 |
+
base_model: jhu-clsp/ettin-encoder-150m
|
| 7 |
+
pipeline_tag: zero-shot-classification
|
| 8 |
+
tags:
|
| 9 |
+
- decision-model
|
| 10 |
+
- system-one
|
| 11 |
+
- falcondec
|
| 12 |
+
- lightdec
|
| 13 |
+
- calibrated-decisions
|
| 14 |
+
- multiple-choice
|
| 15 |
+
- intent-classification
|
| 16 |
+
- customer-support
|
| 17 |
+
- natural-language-inference
|
| 18 |
+
- code
|
| 19 |
+
- guardrails
|
| 20 |
+
- agents
|
| 21 |
+
- selective-prediction
|
| 22 |
+
- falconsai
|
| 23 |
---
|
|
|
|
| 24 |
|
| 25 |
+
# Falconsai/LightDec
|
|
|
|
| 26 |
|
| 27 |
+
**[View in Model Surgeon](https://surgeon.falcons.ai/?hub=Falconsai/LightDec)**
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
**A lightweight, single-pass, typed, calibrated decision model for agentic systems.** Give it a **state** (text, code or JSON), one or more **typed questions** (`choice`, `noul` yes/no, `score` ordinal) and a closed set of options. It returns a calibrated probability for every option, from one encoder pass per question.
|
| 30 |
+
|
| 31 |
+
LightDec is the FalconDec architecture trained with **FalconDec notebook V2** on the **`standard`** data preset. That run adds agent-specific decisions (AgentTrek, Counsel, HotpotQA) to a balanced mix of 58 test tasks across 9 domains. This page is both the **model card** and the **developer guide**: how to load it, test it, and use it as a decision component in an agentic system.
|
| 32 |
+
|
| 33 |
+
> **At a glance.** Test accuracy **0.725** (micro and task-macro) on 17,498 decisions from 58 tasks, with ECE **0.025**. At a 0.70 confidence threshold, LightDec answers **56%** of decisions at **89.6%** accuracy and defers the rest. Weights: **319 MB** fp16, **161 MB** int8. It is strongest on support routing, code understanding, intents, guardrails and agent-step checks, and weakest on multi-step arithmetic, date and table reasoning, and very wide label sets. Evaluate it on your own traffic (§6.4) before acting on its answers.
|
| 34 |
+
|
| 35 |
+
## Contents
|
| 36 |
+
|
| 37 |
+
1. [Model summary](#1-model-summary)
|
| 38 |
+
2. [Results](#2-results)
|
| 39 |
+
3. [Intended and out-of-scope uses](#3-intended-and-out-of-scope-uses)
|
| 40 |
+
4. [The Hub repository](#4-the-hub-repository)
|
| 41 |
+
5. [Install, load and read a result](#5-install-load-and-read-a-result)
|
| 42 |
+
6. [Testing the model](#6-testing-the-model)
|
| 43 |
+
7. [Using it in an agentic system](#7-using-it-in-an-agentic-system)
|
| 44 |
+
8. [Tuning for your domain](#8-tuning-for-your-domain)
|
| 45 |
+
9. [Architecture](#9-architecture)
|
| 46 |
+
10. [Training data](#10-training-data)
|
| 47 |
+
11. [Training procedure and calibration](#11-training-procedure-and-calibration)
|
| 48 |
+
12. [Operational notes](#12-operational-notes)
|
| 49 |
+
13. [Bias, risks and limitations](#13-bias-risks-and-limitations)
|
| 50 |
+
14. [Versioning and lineage](#14-versioning-and-lineage)
|
| 51 |
+
15. [API reference](#15-api-reference)
|
| 52 |
+
16. [Citation and references](#16-citation-and-references)
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## 1. Model summary
|
| 57 |
+
|
| 58 |
+
| | |
|
| 59 |
+
|---|---|
|
| 60 |
+
| **Model** | LightDec: the FalconDec architecture, notebook V2, `standard` preset. The checkpoint's own config reports `FalconDec` version `1.0.0` |
|
| 61 |
+
| **Task** | Closed-set decisions: given a state, a question and 2–N options, return a calibrated probability per option |
|
| 62 |
+
| **Question types** | `choice` (pick one), `noul` (yes/no, returns P(true)), `score` (ordinal rubric, returns the expected level) |
|
| 63 |
+
| **Backbone** | [`jhu-clsp/ettin-encoder-150m`](https://huggingface.co/jhu-clsp/ettin-encoder-150m) (ModernBERT-style encoder), fully fine-tuned |
|
| 64 |
+
| **Decision head** | Option-marker scoring plus a permutation-equivariant set-transformer head (§9) |
|
| 65 |
+
| **Parameters** | ≈160M (Hub reports 0.2B) |
|
| 66 |
+
| **Weights** | fp16 **319 MB** (`model.safetensors`) · per-channel int8 **161 MB** (`compact-int8/model_int8.safetensors`) |
|
| 67 |
+
| **Context** | 512 tokens; automatically 2,048 for questions with more than 24 options; a tournament above 96 options |
|
| 68 |
+
| **Calibration** | One temperature per (question type × option-count bucket), stored in the checkpoint and applied automatically |
|
| 69 |
+
| **Inference cost** | One encoder pass per question. The same architecture (`Falconsai/proof_v3`) measured 15.7 ms p50 for one question on a GPU; LightDec's own latency is not in its report (measure with §6.3) |
|
| 70 |
+
| **Output** | Probabilities, the chosen option, confidence, a `defer` flag, `p_true` (noul) and `expected_level` (score) |
|
| 71 |
+
| **Language** | English, plus code in Python, Java, JavaScript, PHP, Ruby, Go and C |
|
| 72 |
+
| **Custom code** | `falcondec_modeling.py` ships with the weights and holds the model and all inference logic. Load it with `importlib` (§5); `AutoModel.from_pretrained` alone won't build the decision head |
|
| 73 |
+
| **License** | Apache-2.0 for the weights and code. Check each training dataset's license before redistributing derived data |
|
| 74 |
+
|
| 75 |
+
The model has no generative component. It can only rank the options you give it, so it cannot produce text outside that set.
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## 2. Results
|
| 80 |
+
|
| 81 |
+
All numbers come from this checkpoint's `falcondec_report.json`: one run, `standard` preset, 2 epochs, seed 42, `MODE="scratch"`, notebook V2.
|
| 82 |
+
|
| 83 |
+
### 2.1 At a glance
|
| 84 |
+
|
| 85 |
+
| Metric | Value |
|
| 86 |
+
|---|---|
|
| 87 |
+
| Test decisions / tasks | 17,498 / 58 (10 of them held out) |
|
| 88 |
+
| Test accuracy, micro | **0.725** |
|
| 89 |
+
| Test accuracy, task-macro | **0.725** |
|
| 90 |
+
| Held-out tasks, task-macro (10 tasks never trained on) | **0.567** |
|
| 91 |
+
| Expected calibration error (ECE, 15 bins) | **0.025** |
|
| 92 |
+
| Negative log-likelihood / Brier score | 0.652 / 0.358 |
|
| 93 |
+
| Area under the risk–coverage curve (AURC, lower is better) | **0.097** |
|
| 94 |
+
| Ordinal (`score`) mean absolute error, in levels | 0.572 |
|
| 95 |
+
| Coverage / accuracy at confidence ≥ 0.70 | **56.4% / 0.896** |
|
| 96 |
+
| Weights | fp16 319 MB · int8 161 MB |
|
| 97 |
+
|
| 98 |
+
**Selective prediction is the headline.** Calibration is good (ECE 0.025), so the confidence score is a reliable gate. Acting only on decisions with confidence ≥ 0.70 covers 56% of traffic at 89.6% accuracy, against 72.5% accuracy when answering everything. That is the property an agent loop needs: answer the easy majority locally, and hand the rest to an LLM or a human.
|
| 99 |
+
|
| 100 |
+
### 2.2 Per domain
|
| 101 |
+
|
| 102 |
+
| Domain | Test decisions | Task-macro accuracy |
|
| 103 |
+
|---|---|---|
|
| 104 |
+
| support | 600 | **0.997** |
|
| 105 |
+
| code | 2,350 | **0.864** |
|
| 106 |
+
| intents | 1,200 | **0.773** |
|
| 107 |
+
| guardrails | 1,094 | **0.768** |
|
| 108 |
+
| agentic | 717 | **0.754** |
|
| 109 |
+
| workflows | 2,000 | **0.689** |
|
| 110 |
+
| policy | 2,700 | **0.669** |
|
| 111 |
+
| reasoning | 5,637 | **0.667** |
|
| 112 |
+
| classification | 1,200 | **0.584** |
|
| 113 |
+
|
| 114 |
+
### 2.3 Per task
|
| 115 |
+
|
| 116 |
+
*Held-out tasks were never used for training, calibration or model selection. "proof_v2 (card)" lists proof_v2's published score for the same source and task (different samples; indicative only).*
|
| 117 |
+
|
| 118 |
+
| Domain | Task | n | Chance | **Accuracy** | ECE | proof_v2 (card) |
|
| 119 |
+
|---|---|---|---|---|---|---|
|
| 120 |
+
| support | `bitext/route` | 300 | 0.200 | **1.000** | 0.002 | 0.958 |
|
| 121 |
+
| support | `bitext/category` | 300 | 0.172 | **0.993** | 0.008 | |
|
| 122 |
+
| code | `codexglue/lang_id` | 300 | 0.235 | **1.000** | 0.004 | 0.997 |
|
| 123 |
+
| code | `mbpp/solution` | 300 | 0.250 | **0.987** | 0.014 | 0.992 |
|
| 124 |
+
| code | `codexglue/code_to_doc` | 300 | 0.256 | **0.977** | 0.016 | 0.969 |
|
| 125 |
+
| code | `codexglue/doc_to_code` | 300 | 0.274 | **0.973** | 0.022 | 0.961 |
|
| 126 |
+
| code | `codexglue/func_name` | 275 | 0.263 | **0.938** | 0.021 | 0.901 |
|
| 127 |
+
| code | `bigclonebench/clone` | 300 | 0.500 | **0.863** | 0.098 | 0.383 |
|
| 128 |
+
| code | `humaneval/completion` *(held out)* | 119 | 0.394 | **0.756** | 0.152 | 0.575 |
|
| 129 |
+
| code | `mbpp/bugspot` | 156 | 0.413 | **0.731** | 0.086 | 0.475 |
|
| 130 |
+
| code | `devign/vulnerability` | 300 | 0.500 | **0.553** | 0.026 | 0.542 |
|
| 131 |
+
| intents | `banking77/intent` *(held out)* | 300 | 0.317 | **0.923** | 0.037 | 0.883 |
|
| 132 |
+
| intents | `massive_en/intent` | 300 | 0.122 | **0.907** | 0.046 | |
|
| 133 |
+
| intents | `clinc150/intent` | 300 | 0.122 | **0.793** | 0.081 | 0.850 |
|
| 134 |
+
| intents | `banking77/intent_77` *(held out)* | 300 | 0.013 | **0.470** | 0.200 | |
|
| 135 |
+
| guardrails | `jailbreak/detect` | 262 | 0.500 | **0.966** | 0.018 | |
|
| 136 |
+
| guardrails | `civil_comments/toxic` | 300 | 0.500 | **0.807** | 0.051 | |
|
| 137 |
+
| guardrails | `agentharm/refuse` *(held out)* | 416 | 0.500 | **0.654** | 0.178 | |
|
| 138 |
+
| guardrails | `prompt_injections/detect` *(held out)* | 116 | 0.500 | **0.647** | 0.272 | |
|
| 139 |
+
| agentic | `hotpotqa/retrieve` | 298 | 0.168 | **0.842** | 0.078 | |
|
| 140 |
+
| agentic | `hotpotqa/comparison_yes_no` | 17 | 0.500 | **0.824** | 0.185 | |
|
| 141 |
+
| agentic | `counsel/step_has_error` | 201 | 0.500 | **0.791** | 0.093 | |
|
| 142 |
+
| agentic | `counsel/critique_quality` | 201 | 0.333 | **0.557** | 0.094 | |
|
| 143 |
+
| workflows | `typed_decisions/customer_service` | 500 | 0.280 | **0.720** | 0.099 | |
|
| 144 |
+
| workflows | `typed_decisions/security_incidents` | 500 | 0.340 | **0.712** | 0.140 | |
|
| 145 |
+
| workflows | `typed_decisions/agent_trace_observability` | 500 | 0.300 | **0.696** | 0.106 | |
|
| 146 |
+
| workflows | `typed_decisions/invoice_processing` | 500 | 0.350 | **0.628** | 0.101 | |
|
| 147 |
+
| policy | `policy/access_control_transfer` | 300 | 0.333 | **1.000** | 0.001 | |
|
| 148 |
+
| policy | `policy/return_window_transfer` | 300 | 0.333 | **1.000** | 0.019 | |
|
| 149 |
+
| policy | `policy/free_shipping_transfer` | 300 | 0.500 | **0.850** | 0.067 | |
|
| 150 |
+
| policy | `policy/sla_urgency_transfer` | 300 | 0.250 | **0.713** | 0.214 | |
|
| 151 |
+
| policy | `policy/refund_approval_transfer` | 300 | 0.333 | **0.710** | 0.219 | |
|
| 152 |
+
| policy | `policy/count_threshold_transfer` | 300 | 0.179 | **0.523** | 0.085 | |
|
| 153 |
+
| policy | `policy/invoice_total_transfer` | 300 | 0.500 | **0.523** | 0.020 | |
|
| 154 |
+
| policy | `policy/invoice_overdue_transfer` | 300 | 0.500 | **0.407** | 0.364 | |
|
| 155 |
+
| policy | `policy/table_extreme_transfer` | 300 | 0.240 | **0.290** | 0.036 | |
|
| 156 |
+
| reasoning | `qasc/mcq` | 300 | 0.125 | **0.983** | 0.007 | |
|
| 157 |
+
| reasoning | `snli/must_be_true` | 300 | 0.333 | **0.970** | 0.029 | 0.908 |
|
| 158 |
+
| reasoning | `snli/contradicts` | 300 | 0.333 | **0.967** | 0.035 | 0.892 |
|
| 159 |
+
| reasoning | `scitail/support` | 300 | 0.500 | **0.957** | 0.030 | |
|
| 160 |
+
| reasoning | `sciq/mcq` | 300 | 0.250 | **0.950** | 0.023 | 0.692 |
|
| 161 |
+
| reasoning | `snli/nli` | 594 | 0.333 | **0.837** | 0.035 | |
|
| 162 |
+
| reasoning | `mnli/claim` | 300 | 0.333 | **0.807** | 0.072 | 0.492 |
|
| 163 |
+
| reasoning | `boolq/yes_no` | 300 | 0.500 | **0.783** | 0.071 | 0.717 |
|
| 164 |
+
| reasoning | `gsm8k/math` | 300 | 0.250 | **0.637** | 0.050 | 0.275 |
|
| 165 |
+
| reasoning | `commonsense_qa/mcq` | 296 | 0.200 | **0.611** | 0.058 | 0.442 |
|
| 166 |
+
| reasoning | `arc_easy/mcq` *(held out)* | 300 | 0.250 | **0.553** | 0.057 | 0.425 |
|
| 167 |
+
| reasoning | `openbookqa/mcq` | 300 | 0.250 | **0.550** | 0.068 | 0.292 |
|
| 168 |
+
| reasoning | `winogrande/blank` | 300 | 0.500 | **0.540** | 0.089 | |
|
| 169 |
+
| reasoning | `arc_challenge/mcq` *(held out)* | 300 | 0.250 | **0.423** | 0.085 | 0.308 |
|
| 170 |
+
| reasoning | `anli/nli` | 300 | 0.333 | **0.393** | 0.185 | |
|
| 171 |
+
| reasoning | `mmlu/mcq` *(held out)* | 300 | 0.250 | **0.393** | 0.094 | |
|
| 172 |
+
| reasoning | `hellaswag/continuation` | 300 | 0.250 | **0.383** | 0.171 | |
|
| 173 |
+
| reasoning | `aqua_rat/math` | 247 | 0.200 | **0.259** | 0.071 | |
|
| 174 |
+
| classification | `ag_news/topic` | 300 | 0.250 | **0.847** | 0.051 | |
|
| 175 |
+
| classification | `yelp/score` | 300 | 0.200 | **0.640** | 0.069 | |
|
| 176 |
+
| classification | `emotion/6way` *(held out)* | 300 | 0.167 | **0.480** | 0.049 | |
|
| 177 |
+
| classification | `sst5/score` *(held out)* | 300 | 0.200 | **0.370** | 0.086 | |
|
| 178 |
+
|
| 179 |
+
### 2.4 Comparison with proof_v2 (indicative)
|
| 180 |
+
|
| 181 |
+
On the 22 tasks that both this report and proof_v2's model card cover, LightDec's task-macro accuracy is **0.807 vs 0.679**, and it scores higher on **20 of 22**. The largest gains are on the tasks proof_v2 reported as weak:
|
| 182 |
+
|
| 183 |
+
| Task | proof_v2 (card) | LightDec |
|
| 184 |
+
|---|---|---|
|
| 185 |
+
| BigCloneBench clone detection | 0.383 | **0.863** |
|
| 186 |
+
| GSM8K (4-option numeric) | 0.275 | **0.637** |
|
| 187 |
+
| MultiNLI claim | 0.492 | **0.807** |
|
| 188 |
+
| OpenBookQA | 0.292 | **0.550** |
|
| 189 |
+
| MBPP bug spotting | 0.475 | **0.731** |
|
| 190 |
+
| HumanEval completion *(held out)* | 0.575 | **0.756** |
|
| 191 |
+
| ARC-Challenge *(held out)* | 0.308 | **0.423** |
|
| 192 |
+
|
| 193 |
+
LightDec is lower on CLINC150 (0.793 vs 0.850) and MBPP task→solution (0.987 vs 0.992).
|
| 194 |
+
|
| 195 |
+
These are **different test samples and, for some tasks, different question formats**. For example, LightDec's MultiNLI task is three-way NLI, and its bug-spotting mutants are verified to fail the unit tests. The like-for-like head-to-head, which runs proof_v2 on identical decisions (notebook cell 23), **did not run** for this checkpoint (`head_to_head: null`).
|
| 196 |
+
|
| 197 |
+
### 2.5 Comparison with Laya and TypeSafe Jev (indicative)
|
| 198 |
+
|
| 199 |
+
| Benchmark | LightDec | Laya | TypeSafe Jev 1.13.0 |
|
| 200 |
+
|---|---|---|---|
|
| 201 |
+
| typed-decisions test (2,000 decisions, 4 workflows) | 0.689 | 0.766 (fine-tuned) · 0.362 (zero-shot) | 0.727 |
|
| 202 |
+
| AG News (4 labels) | 0.847 | 0.950 | 0.910 |
|
| 203 |
+
| DAIR Emotion, 6 labels *(held out for LightDec)* | 0.480 | 0.595 | 0.480 |
|
| 204 |
+
| Banking77, all 77 labels in one question *(held out)* | 0.470 | 0.425 | 0.870 (72 labels) |
|
| 205 |
+
| SST-5 (ordinal) *(held out)* | 0.370 | 0.372 | — |
|
| 206 |
+
|
| 207 |
+
Laya's numbers are from its own benchmark report; Jev's are third-party published. LightDec was trained on the typed-decisions training split, like the fine-tuned Laya checkpoint. **On typed-decisions LightDec trails both** (the teacher-agreement ceiling is 0.735 and the majority-class baseline 0.461). It matches Jev on Emotion, edges Laya on all-77 Banking77, and trails both on AG News. LightDec is 2.6× smaller than Laya's 421M English checkpoint.
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
|
| 211 |
+
## 3. Intended and out-of-scope uses
|
| 212 |
+
|
| 213 |
+
### Intended
|
| 214 |
+
|
| 215 |
+
| Use | Measured evidence |
|
| 216 |
+
|---|---|
|
| 217 |
+
| **Support and ticket routing** | Bitext route 1.000, Bitext category 0.993; Banking77 (held out, 2–5 options) 0.923; MASSIVE 0.907 |
|
| 218 |
+
| **Code understanding against a menu** | Language ID 1.000; code↔description 0.973–0.977; task→solution 0.987; function naming 0.938 |
|
| 219 |
+
| **Guardrails** | Jailbreak detection 0.966; toxicity 0.807. Held out: prompt-injection 0.647, AgentHarm refusal 0.654, so recalibrate and test on your own traffic |
|
| 220 |
+
| **Agent loops** | Retrieval routing (HotpotQA) 0.842; "does this agent step contain an error?" (Counsel) 0.791 |
|
| 221 |
+
| **Statement verification** | SNLI must-be-true / contradicts 0.970 / 0.967; SciTail 0.957; SciQ 0.950 |
|
| 222 |
+
| **Selective automation** | 89.6% accuracy on the 56% of decisions with confidence ≥ 0.70 |
|
| 223 |
+
|
| 224 |
+
### Out of scope
|
| 225 |
+
|
| 226 |
+
- **Multi-step arithmetic and quantitative reasoning.** AQuA 0.259 (chance 0.20); counting and summing policies 0.52; comparing values in a table 0.290. GSM8K reaches 0.637 only because it is posed as 4-option multiple choice with near-miss distractors. Route real math to an LLM or code.
|
| 227 |
+
- **Date reasoning in unfamiliar formats.** The invoice-overdue transfer test (ISO dates, whereas training used "Month DD, YYYY") scores **0.407, below chance, with ECE 0.364**. It is confidently wrong there. Normalise dates before asking, or compute them in code.
|
| 228 |
+
- **Very wide label sets.** All 77 Banking77 intents in one question score 0.470. Pre-filter to a shortlist of about 20 options (§7.1).
|
| 229 |
+
- **Hard commonsense and exam knowledge**: HellaSwag 0.383, ANLI 0.393, MMLU 0.393.
|
| 230 |
+
- **Code security and correctness gating**: Devign 0.553 is near chance. Don't use it to approve code.
|
| 231 |
+
- **Open-ended questions**: it always picks one of your options. Add "None of the above" when appropriate; it was trained with that option.
|
| 232 |
+
- **Non-English text**, and **high-stakes decisions without human oversight**.
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## 4. The Hub repository
|
| 237 |
+
|
| 238 |
+
| File | Contents |
|
| 239 |
+
|---|---|
|
| 240 |
+
| `model.safetensors` | fp16 weights (319 MB): encoder, decision head and the temperature buffer |
|
| 241 |
+
| `falcondec_config.json` | Layout (sequence lengths, option budgets), special-token ids, temperatures, defer threshold, version, lineage |
|
| 242 |
+
| `encoder/` | Backbone configuration (the encoder is rebuilt from this, then the weights are loaded) |
|
| 243 |
+
| `tokenizer/` | Tokenizer files |
|
| 244 |
+
| `falcondec_modeling.py` | `FalconDec`, `load_falcondec`, `decide`, `score_items`, `save_falcondec` and the int8 codec |
|
| 245 |
+
| `falcondec_report.json` | Training configuration, data counts, history, temperatures and all test results |
|
| 246 |
+
| `compact-int8/` | The same model with per-channel int8 weights (`model_int8.safetensors`, 161 MB); a complete, self-contained directory with its own config, tokenizer and modeling file |
|
| 247 |
+
| `README.md` | This card |
|
| 248 |
+
|
| 249 |
+
**Pin a revision in production.** The repo can change, so pass a commit hash when you load.
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## 5. Install, load and read a result
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
pip install torch "transformers>=4.48" safetensors huggingface_hub numpy
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
### 5.1 Load
|
| 260 |
+
|
| 261 |
+
Save this helper as `lightdec.py` next to your code. Every example below uses it.
|
| 262 |
|
| 263 |
```python
|
| 264 |
+
# lightdec.py
|
| 265 |
+
import importlib.util, json, shutil
|
| 266 |
+
from pathlib import Path
|
| 267 |
from huggingface_hub import snapshot_download
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def load_lightdec(repo="Falconsai/LightDec", revision=None, variant="fp16", device=None, dtype=None):
|
| 271 |
+
"""Returns (fdm, model, tokenizer). variant: "fp16" (319 MB) or "int8" (161 MB, dequantised on load)."""
|
| 272 |
+
path = Path(repo) if Path(repo).exists() else Path(snapshot_download(repo, revision=revision))
|
| 273 |
+
if variant == "int8":
|
| 274 |
+
path = path / "compact-int8"
|
| 275 |
+
fc = json.loads((path / "falcondec_config.json").read_text(encoding="utf-8"))
|
| 276 |
+
expected = fc.get("weights", "model.safetensors")
|
| 277 |
+
if not (path / expected).exists(): # e.g. a renamed weight file in a processed copy
|
| 278 |
+
cands = sorted(path.glob("*.safetensors"))
|
| 279 |
+
if not cands:
|
| 280 |
+
raise FileNotFoundError(f"no .safetensors weights in {path}")
|
| 281 |
+
local = Path("lightdec_local") / variant
|
| 282 |
+
shutil.copytree(path, local, dirs_exist_ok=True)
|
| 283 |
+
shutil.copy(cands[0], local / expected)
|
| 284 |
+
path = local
|
| 285 |
+
spec = importlib.util.spec_from_file_location("falcondec_modeling", str(path / "falcondec_modeling.py"))
|
| 286 |
+
fdm = importlib.util.module_from_spec(spec)
|
| 287 |
+
spec.loader.exec_module(fdm)
|
| 288 |
+
model, tok = fdm.load_falcondec(str(path), device=device, dtype=dtype) # cuda if available, else cpu
|
| 289 |
+
return fdm, model, tok
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
```python
|
| 293 |
+
from lightdec import load_lightdec
|
| 294 |
+
|
| 295 |
+
fdm, model, tok = load_lightdec() # or load_lightdec(revision="<commit>", variant="int8")
|
| 296 |
+
print(model.fcfg["name"], model.fcfg["version"], round(model.num_parameters() / 1e6, 1), "M params")
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
If loading prints `[FalconDec] load warning: missing=… unexpected=…`, the weights didn't match the architecture. Treat that as a failed load (§6.1 checks for it).
|
| 300 |
+
|
| 301 |
+
### 5.2 Decide
|
| 302 |
+
|
| 303 |
+
`decide()` takes one state and any number of typed questions: either a list, or a Jev/Laya-style dict keyed by name.
|
| 304 |
+
|
| 305 |
+
```python
|
| 306 |
+
state = {"from": "user@acme.com", "subject": "Duplicate charge on invoice #4411",
|
| 307 |
+
"body": "We were billed twice for March. Please refund the duplicate today or we will cancel our plan."}
|
| 308 |
+
|
| 309 |
+
out = fdm.decide(model, tok, state, {
|
| 310 |
+
"department": {"type": "choice", "instructions": "Which department should handle this request?",
|
| 311 |
+
"criteria": {"billing": "invoices, payments, refunds", "technical": "bugs, outages",
|
| 312 |
+
"sales": "pricing, contracts", "other": "everything else"}},
|
| 313 |
+
"urgency": {"type": "score", "instructions": "How urgent is this request?",
|
| 314 |
+
"criteria": ["not urgent", "soon", "critical deadline or blocking issue"]},
|
| 315 |
+
"churn_risk": {"type": "noul", "instructions": "Does the user threaten to cancel or leave?"},
|
| 316 |
+
})
|
| 317 |
+
a = out["answers"]
|
| 318 |
+
print(a["department"]["choice"], round(a["department"]["confidence"], 3), a["department"]["defer"])
|
| 319 |
+
print("urgency level", round(a["urgency"]["expected_level"], 2), "of", 2)
|
| 320 |
+
print("P(churn)", round(a["churn_risk"]["p_true"], 3))
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
Plain options work too: `{"question": "Which team?", "options": ["Accounts", "Billing", "Shipping"]}`.
|
| 324 |
+
|
| 325 |
+
### 5.3 Reading a result
|
| 326 |
+
|
| 327 |
+
Each item in `out["results"]` (and `out["answers"][key]`) contains:
|
| 328 |
+
|
| 329 |
+
| Field | Meaning |
|
| 330 |
+
|---|---|
|
| 331 |
+
| `key` | The question's name (dict input) or `None` |
|
| 332 |
+
| `type` | `choice`, `noul` or `score` |
|
| 333 |
+
| `choice` | The chosen key: the criteria key, the option text, `True`/`False` for `noul`, or the level index for `score` |
|
| 334 |
+
| `choice_text` | The option text the model saw |
|
| 335 |
+
| `confidence` | Calibrated probability of `choice` |
|
| 336 |
+
| `probs` | The full distribution, keyed by `str(key)` |
|
| 337 |
+
| `defer` | `True` when `confidence` is below the defer threshold (default 0.70, stored in the config): don't act on it (§7.3) |
|
| 338 |
+
| `p_true` | `noul` only: calibrated P(yes) |
|
| 339 |
+
| `expected_level` | `score` only: probability-weighted level (0 … k−1); better than the argmax for ordinal rubrics (test MAE 0.57 levels) |
|
| 340 |
+
|
| 341 |
+
How to read them:
|
| 342 |
+
|
| 343 |
+
- **Low confidence, spread probabilities**: the state doesn't support any option clearly. Defer, or add "None of the above".
|
| 344 |
+
- **`noul` near 0.5**: genuinely ambiguous. Ask for more information rather than guessing.
|
| 345 |
+
- **`score`**: use `expected_level` for thresholds ("escalate if ≥ 1.5") rather than `choice`.
|
| 346 |
+
|
| 347 |
+
---
|
| 348 |
+
|
| 349 |
+
## 6. Testing the model
|
| 350 |
+
|
| 351 |
+
Tests 6.1–6.3 need no labelled data, so run them in CI whenever you change the revision. Test 6.4 is the one that tells you whether to ship.
|
| 352 |
+
|
| 353 |
+
### 6.1 Integrity and determinism
|
| 354 |
+
|
| 355 |
+
Save as `check_lightdec.py` and run `python check_lightdec.py [revision]`.
|
| 356 |
+
|
| 357 |
+
```python
|
| 358 |
+
import contextlib, io, sys
|
| 359 |
+
import numpy as np
|
| 360 |
+
from lightdec import load_lightdec
|
| 361 |
+
|
| 362 |
+
rev = sys.argv[1] if len(sys.argv) > 1 else None
|
| 363 |
+
log = io.StringIO()
|
| 364 |
+
with contextlib.redirect_stdout(log):
|
| 365 |
+
fdm, model, tok = load_lightdec(revision=rev)
|
| 366 |
+
assert "load warning" not in log.getvalue(), log.getvalue()
|
| 367 |
+
|
| 368 |
+
fc = model.fcfg
|
| 369 |
+
assert fc["name"] == "FalconDec", fc["name"] # LightDec checkpoints use the FalconDec architecture name
|
| 370 |
+
T = model.temperature.float().cpu().numpy()
|
| 371 |
+
assert T.shape == (3, 4) and (T > 0).all(), T
|
| 372 |
+
q = {"team": {"question": "Which team?", "options": ["recover password", "shipping", "invoicing"]}}
|
| 373 |
+
a = fdm.decide(model, tok, "I forgot my password and can't sign in.", q)["answers"]["team"]
|
| 374 |
+
b = fdm.decide(model, tok, "I forgot my password and can't sign in.", q)["answers"]["team"]
|
| 375 |
+
assert all(abs(a["probs"][k] - b["probs"][k]) < 1e-4 for k in a["probs"]), "non-deterministic"
|
| 376 |
+
assert abs(sum(a["probs"].values()) - 1) < 1e-3
|
| 377 |
+
print(f"OK LightDec (FalconDec v{fc['version']}, notebook {fc.get('notebook_version')}) choice={a['choice']} "
|
| 378 |
+
f"conf={a['confidence']:.3f} defer_threshold={fc.get('defer_threshold')}")
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
The stored temperatures should read approximately `[[1.707, 1.352, 1.466, 1.349], [1.402 ×4], [1.453 ×4]]` (§11.2).
|
| 382 |
+
|
| 383 |
+
### 6.2 Behavioural tests (pytest)
|
| 384 |
+
|
| 385 |
+
Save as `tests/test_lightdec.py` and run `pytest -q`. Set `LIGHTDEC_REVISION` to test a pinned commit.
|
| 386 |
+
|
| 387 |
+
```python
|
| 388 |
+
import os, random
|
| 389 |
+
import pytest
|
| 390 |
+
from lightdec import load_lightdec
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
@pytest.fixture(scope="session")
|
| 394 |
+
def fd():
|
| 395 |
+
return load_lightdec(revision=os.environ.get("LIGHTDEC_REVISION"))
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def ask(fd, state, question, options, **kw):
|
| 399 |
+
fdm, model, tok = fd
|
| 400 |
+
return fdm.decide(model, tok, state, [dict(question=question, options=options, **kw)])["results"][0]
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def test_probabilities_are_valid(fd):
|
| 404 |
+
r = ask(fd, "The build failed on main.", "What next?", ["Revert", "Ignore", "Retry"])
|
| 405 |
+
assert all(0 <= p <= 1 for p in r["probs"].values()) and abs(sum(r["probs"].values()) - 1) < 1e-3
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def test_typed_outputs(fd):
|
| 409 |
+
fdm, model, tok = fd
|
| 410 |
+
out = fdm.decide(model, tok, "I was charged twice. Refund me or I'm leaving.", {
|
| 411 |
+
"refund": {"type": "noul", "instructions": "Does the user ask for a refund?"},
|
| 412 |
+
"urgency": {"type": "score", "instructions": "How urgent?", "criteria": ["low", "medium", "high"]}})["answers"]
|
| 413 |
+
assert 0 <= out["refund"]["p_true"] <= 1 and out["refund"]["choice"] in (True, False)
|
| 414 |
+
assert 0 <= out["urgency"]["expected_level"] <= 2
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def test_support_routing(fd):
|
| 418 |
+
r = ask(fd, "I forgot my password and the reset email never arrived.", "Which team should handle this?",
|
| 419 |
+
["recover password", "billing and payment", "delivery information"])
|
| 420 |
+
assert r["choice"] == "recover password"
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def test_fanout_matches_single_questions(fd):
|
| 424 |
+
# Batching changes padding; under bf16 that moves probabilities slightly, never the substance.
|
| 425 |
+
fdm, model, tok = fd
|
| 426 |
+
state = "I forgot my password and can't sign in."
|
| 427 |
+
qs = [{"question": "Team?", "options": ["recover password", "shipping", "invoicing"]},
|
| 428 |
+
{"question": "Urgent?", "options": ["Yes", "No"]}]
|
| 429 |
+
together = fdm.decide(model, tok, state, qs)["results"]
|
| 430 |
+
for q, t in zip(qs, together):
|
| 431 |
+
alone = fdm.decide(model, tok, state, [q])["results"][0]
|
| 432 |
+
assert all(abs(alone["probs"][k] - t["probs"][k]) < 2e-2 for k in alone["probs"])
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def test_option_order_is_mostly_irrelevant(fd):
|
| 436 |
+
# The head is order-equivariant, but the encoder sees positions; training reshuffled options every epoch.
|
| 437 |
+
state, q = "Where is my parcel? It's three days late.", "What should support do?"
|
| 438 |
+
opts = ["Give the delivery status", "Start a refund", "Book an appointment"]
|
| 439 |
+
base = ask(fd, state, q, opts)["choice"]
|
| 440 |
+
same = sum(ask(fd, state, q, random.Random(s).sample(opts, len(opts)))["choice"] == base for s in range(5))
|
| 441 |
+
assert same >= 4
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def test_many_options_use_the_tournament(fd):
|
| 445 |
+
opts = [f"topic number {i}" for i in range(119)] + ["reset my password"]
|
| 446 |
+
r = ask(fd, "I can't log in, I need to reset my password.", "What does the user want?", opts)
|
| 447 |
+
assert len(r["probs"]) == 120 and abs(sum(r["probs"].values()) - 1) < 1e-3
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def test_int8_agrees_with_fp16(fd):
|
| 451 |
+
fdm8, m8, tok8 = load_lightdec(revision=os.environ.get("LIGHTDEC_REVISION"), variant="int8")
|
| 452 |
+
fdm, model, tok = fd
|
| 453 |
+
items = [dict(state=s, question="Which team?", options=["billing", "shipping", "accounts", "technical"])
|
| 454 |
+
for s in ["I was double charged", "Where is my parcel?", "Change my email", "The app crashes on start",
|
| 455 |
+
"Refund the duplicate payment", "Package never arrived", "Reset my login", "Error 500 on checkout"]]
|
| 456 |
+
a = [p.argmax() for p in fdm.score_items(model, tok, items)]
|
| 457 |
+
b = [p.argmax() for p in fdm8.score_items(m8, tok8, items)]
|
| 458 |
+
assert sum(x == y for x, y in zip(a, b)) >= len(items) - 1
|
| 459 |
+
```
|
| 460 |
+
|
| 461 |
+
### 6.3 Latency
|
| 462 |
+
|
| 463 |
+
```python
|
| 464 |
+
import time, numpy as np, torch
|
| 465 |
+
from lightdec import load_lightdec
|
| 466 |
+
|
| 467 |
+
fdm, model, tok = load_lightdec()
|
| 468 |
+
q = [{"question": "Route?", "options": ["billing and payment", "shipping", "recover password"]}]
|
| 469 |
+
for _ in range(5):
|
| 470 |
+
fdm.decide(model, tok, "I was charged twice.", q)
|
| 471 |
+
t = []
|
| 472 |
+
for _ in range(100):
|
| 473 |
+
if torch.cuda.is_available(): torch.cuda.synchronize()
|
| 474 |
+
t0 = time.perf_counter(); fdm.decide(model, tok, "I was charged twice.", q)
|
| 475 |
+
if torch.cuda.is_available(): torch.cuda.synchronize()
|
| 476 |
+
t.append((time.perf_counter() - t0) * 1000)
|
| 477 |
+
print(f"p50 {np.percentile(t, 50):.1f} ms p95 {np.percentile(t, 95):.1f} ms on {model.device}")
|
| 478 |
+
```
|
| 479 |
+
|
| 480 |
+
For CPU serving, load the int8 variant with `device="cpu", dtype=torch.float32`, and optionally apply `torch.ao.quantization.quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8)` for int8 matrix multiplies.
|
| 481 |
+
|
| 482 |
+
### 6.4 Accuracy on your own labelled data
|
| 483 |
+
|
| 484 |
+
Write 50–500 decisions that look like your real traffic, one JSON object per line. `expected` may be a letter, a 0-based index or the option text; `type` is optional.
|
| 485 |
+
|
| 486 |
+
```json
|
| 487 |
+
{"id": "t1", "tag": "support", "state": "…", "question": "…", "options": ["…", "…"], "expected": "B", "type": "choice"}
|
| 488 |
+
```
|
| 489 |
+
|
| 490 |
+
```python
|
| 491 |
+
import json, string
|
| 492 |
+
import numpy as np
|
| 493 |
+
from lightdec import load_lightdec
|
| 494 |
+
|
| 495 |
+
fdm, model, tok = load_lightdec()
|
| 496 |
+
rows = [json.loads(l) for l in open("my_eval.jsonl", encoding="utf-8") if l.strip()]
|
| 497 |
+
|
| 498 |
+
def idx(v, opts):
|
| 499 |
+
if isinstance(v, int): return v
|
| 500 |
+
v = str(v).strip()
|
| 501 |
+
if len(v) == 1 and v.upper() in string.ascii_uppercase[:len(opts)]: return string.ascii_uppercase.index(v.upper())
|
| 502 |
+
return [o.lower() for o in opts].index(v.lower())
|
| 503 |
+
|
| 504 |
+
items = [dict(state=r["state"], question=r.get("question", ""), options=r["options"], type=r.get("type", "choice"))
|
| 505 |
+
for r in rows]
|
| 506 |
+
probs = fdm.score_items(model, tok, items, batch_size=64)
|
| 507 |
+
gold = np.array([idx(r["expected"], r["options"]) for r in rows])
|
| 508 |
+
pred = np.array([p.argmax() for p in probs]); conf = np.array([p.max() for p in probs]); ok = pred == gold
|
| 509 |
+
|
| 510 |
+
def ece(c, k, bins=15):
|
| 511 |
+
e = 0.0
|
| 512 |
+
for lo in np.linspace(0, 1, bins, endpoint=False):
|
| 513 |
+
m = (c > lo) & (c <= lo + 1 / bins)
|
| 514 |
+
if m.any(): e += m.mean() * abs(c[m].mean() - k[m].mean())
|
| 515 |
+
return e
|
| 516 |
+
|
| 517 |
+
print(f"accuracy {ok.mean():.3f} | ECE {ece(conf, ok):.3f}")
|
| 518 |
+
for tag in sorted({r.get("tag", "all") for r in rows}):
|
| 519 |
+
m = np.array([r.get("tag", "all") == tag for r in rows]); print(f" {tag:12s} n={m.sum():4d} acc={ok[m].mean():.3f}")
|
| 520 |
+
for t in (0.5, 0.6, 0.7, 0.8, 0.9):
|
| 521 |
+
m = conf >= t
|
| 522 |
+
print(f" act if conf >= {t}: answers {m.mean():6.1%}, accuracy when answering {ok[m].mean() if m.any() else float('nan'):.3f}")
|
| 523 |
+
```
|
| 524 |
+
|
| 525 |
+
The last loop is the deferral policy of §7.3; on the published test mix, 0.70 gives 56% coverage at 0.896 accuracy. Pick the smallest threshold whose "accuracy when answering" meets your bar. If ECE on your data is much higher than 0.025, recalibrate (§8.2).
|
| 526 |
+
|
| 527 |
+
### 6.5 Regression gate between revisions
|
| 528 |
+
|
| 529 |
+
Fail the pipeline if any test task drops by more than two points between two revisions:
|
| 530 |
+
|
| 531 |
+
```python
|
| 532 |
+
import json, sys
|
| 533 |
+
from huggingface_hub import hf_hub_download
|
| 534 |
+
|
| 535 |
+
old_rev, new_rev = sys.argv[1:3]
|
| 536 |
+
rep = lambda rev: {r["task"]: r["acc"] for r in json.load(open(
|
| 537 |
+
hf_hub_download("Falconsai/LightDec", "falcondec_report.json", revision=rev), encoding="utf-8"))["test_per_task"]}
|
| 538 |
+
old, new = rep(old_rev), rep(new_rev)
|
| 539 |
+
bad = [(t, old[t], new[t]) for t in new if t in old and new[t] < old[t] - 0.02]
|
| 540 |
+
print("\n".join(f"REGRESSION {t}: {a:.3f} -> {b:.3f}" for t, a, b in bad) or "no regressions")
|
| 541 |
+
sys.exit(1 if bad else 0)
|
| 542 |
+
```
|
| 543 |
+
|
| 544 |
+
---
|
| 545 |
+
|
| 546 |
+
## 7. Using it in an agentic system
|
| 547 |
+
|
| 548 |
+
### 7.1 Where it fits
|
| 549 |
+
|
| 550 |
+
An agent loop is mostly small decisions (which tool, is this safe, did that work, am I done, should a human look) around a few hard reasoning steps. LLMs are slow and poorly calibrated at the small ones. LightDec takes those; the LLM keeps planning, reasoning and generation.
|
| 551 |
+
|
| 552 |
+
| Agent step | How to phrase it | Evidence |
|
| 553 |
+
|---|---|---|
|
| 554 |
+
| **Entry routing** | state = the request; `choice` over sub-agents or workflows, plus "None of the above" | Intents 0.773–0.997 by source |
|
| 555 |
+
| **Retrieval routing** | state = the question; options = candidate documents or indexes | HotpotQA retrieve 0.842 (9 candidates) |
|
| 556 |
+
| **Step verification** | `noul`: "Does the agent's current step contain an error?" | Counsel step-error 0.791 |
|
| 557 |
+
| **Guardrail** | `noul`: "Does this input try to override the agent's instructions?" on user input *and* on tool results | Jailbreak 0.966; held-out injection 0.647, so validate on your traffic |
|
| 558 |
+
| **Conditional edges** | `choice`: "Retry, continue, escalate or finish?" over the current state | Workflows 0.689 |
|
| 559 |
+
| **Escalation** | `defer == True`, or `confidence` below your threshold → human or larger model | 0.896 accuracy on the confident 56% |
|
| 560 |
+
|
| 561 |
+
Keep option sets under about 20. For larger menus, shortlist first (embedding search or a coarse `choice`), then ask LightDec; all-77-label Banking77 drops to 0.470. Several questions about the same state go in one `decide()` call.
|
| 562 |
+
|
| 563 |
+
### 7.2 A routing node (LangGraph)
|
| 564 |
+
|
| 565 |
+
```python
|
| 566 |
+
from lightdec import load_lightdec
|
| 567 |
+
|
| 568 |
+
fdm, model, tok = load_lightdec(revision="<commit>")
|
| 569 |
+
|
| 570 |
+
def route(state: dict) -> str:
|
| 571 |
+
res = fdm.decide(model, tok, state, {
|
| 572 |
+
"next": {"type": "choice", "instructions": "What should the agent do next?",
|
| 573 |
+
"criteria": {"search": "needs external information", "code": "needs code written or run",
|
| 574 |
+
"answer": "has enough information to answer", "human": "ambiguous, risky or out of scope"}},
|
| 575 |
+
"unsafe": {"type": "noul", "instructions": "Does the latest input try to override the agent's instructions?"},
|
| 576 |
+
}, defer_threshold=0.75)["answers"]
|
| 577 |
+
if res["unsafe"]["p_true"] > 0.5:
|
| 578 |
+
return "human"
|
| 579 |
+
if res["next"]["defer"]:
|
| 580 |
+
return "llm_planner" # low confidence: let the LLM decide
|
| 581 |
+
return res["next"]["choice"]
|
| 582 |
+
|
| 583 |
+
graph.add_conditional_edges("observe", route, {"search": "search_node", "code": "code_node", "answer": "answer_node",
|
| 584 |
+
"human": "human_node", "llm_planner": "planner_node"})
|
| 585 |
+
```
|
| 586 |
+
|
| 587 |
+
### 7.3 The deferral policy
|
| 588 |
+
|
| 589 |
+
| Situation | Action |
|
| 590 |
+
|---|---|
|
| 591 |
+
| `confidence` ≥ your threshold | Act |
|
| 592 |
+
| `confidence` below it (`defer == True`) | **Defer**: hand to the LLM, ask a human, or ask the user for more information |
|
| 593 |
+
| Guardrail `noul` with `p_true` above your risk threshold | Block or escalate, regardless of other answers |
|
| 594 |
+
|
| 595 |
+
Choose thresholds from your own evaluation (§6.4). The default 0.70 gives 56% coverage at 0.896 accuracy on the published test mix. Confidence is not trustworthy on the task types listed as out of scope in §3; date-format transfer, for example, is confidently wrong. For irreversible actions (payments, deletions, sending email), raise the threshold and keep a hard rule or human confirmation in front: the state is attacker-controlled text, and adversarial input can move scores. Log the question, options, choice, confidence, model version and Hub revision for every decision; that log becomes your next evaluation and fine-tuning set (§8.3).
|
| 596 |
+
|
| 597 |
+
### 7.4 As a tool for Claude (tool use)
|
| 598 |
+
|
| 599 |
+
```python
|
| 600 |
+
import json, threading
|
| 601 |
+
import anthropic
|
| 602 |
+
from lightdec import load_lightdec
|
| 603 |
+
|
| 604 |
+
fdm, model, tok = load_lightdec()
|
| 605 |
+
lock = threading.Lock()
|
| 606 |
+
client = anthropic.Anthropic()
|
| 607 |
+
tools = [{
|
| 608 |
+
"name": "lightdec_decide",
|
| 609 |
+
"description": ("Fast, local, calibrated closed-set decision model. Give it a state (text or JSON), a question and "
|
| 610 |
+
"2-20 distinct options; it returns the choice, a calibrated confidence and a 'defer' flag. "
|
| 611 |
+
"If 'defer' is true, don't rely on the answer. Not for arithmetic, dates or multi-step reasoning."),
|
| 612 |
+
"input_schema": {"type": "object", "properties": {
|
| 613 |
+
"state": {"type": "string", "description": "The message, document excerpt or JSON state."},
|
| 614 |
+
"question": {"type": "string"},
|
| 615 |
+
"options": {"type": "array", "items": {"type": "string"}, "minItems": 2},
|
| 616 |
+
"type": {"type": "string", "enum": ["choice", "score"], "description": "score = options are ordered levels"}},
|
| 617 |
+
"required": ["state", "question", "options"]},
|
| 618 |
+
}]
|
| 619 |
+
|
| 620 |
+
def run_tool(inp):
|
| 621 |
+
with lock:
|
| 622 |
+
r = fdm.decide(model, tok, inp["state"], [{"question": inp["question"], "options": inp["options"],
|
| 623 |
+
"type": inp.get("type", "choice")}])["results"][0]
|
| 624 |
+
return {k: r[k] for k in ("choice", "confidence", "defer", "probs") if k in r}
|
| 625 |
+
|
| 626 |
+
messages = [{"role": "user", "content": "Triage: 'I forgot my password and the reset email never arrived.' "
|
| 627 |
+
"Teams: Accounts, Billing, Shipping."}]
|
| 628 |
+
while True:
|
| 629 |
+
resp = client.messages.create(model="claude-sonnet-5", max_tokens=1024, tools=tools, messages=messages)
|
| 630 |
+
if resp.stop_reason != "tool_use":
|
| 631 |
+
print("".join(b.text for b in resp.content if b.type == "text"))
|
| 632 |
+
break
|
| 633 |
+
messages.append({"role": "assistant", "content": resp.content})
|
| 634 |
+
results = []
|
| 635 |
+
for block in resp.content:
|
| 636 |
+
if block.type == "tool_use" and block.name == "lightdec_decide":
|
| 637 |
+
try:
|
| 638 |
+
results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(run_tool(block.input))})
|
| 639 |
+
except Exception as exc:
|
| 640 |
+
results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(exc), "is_error": True})
|
| 641 |
+
messages.append({"role": "user", "content": results})
|
| 642 |
+
```
|
| 643 |
+
|
| 644 |
+
### 7.5 As an MCP server
|
| 645 |
+
|
| 646 |
+
`pip install mcp`, then save `lightdec_mcp.py` next to `lightdec.py`:
|
| 647 |
+
|
| 648 |
+
```python
|
| 649 |
+
import os, threading
|
| 650 |
+
from mcp.server.fastmcp import FastMCP
|
| 651 |
+
from lightdec import load_lightdec
|
| 652 |
+
|
| 653 |
+
fdm, model, tok = load_lightdec(revision=os.environ.get("LIGHTDEC_REVISION"),
|
| 654 |
+
variant=os.environ.get("LIGHTDEC_VARIANT", "fp16"))
|
| 655 |
+
MIN_CONF = float(os.environ.get("LIGHTDEC_MIN_CONF", "0.7"))
|
| 656 |
+
lock = threading.Lock()
|
| 657 |
+
mcp = FastMCP("lightdec")
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
@mcp.tool()
|
| 661 |
+
def decide(state: str, question: str, options: list[str], type: str = "choice") -> dict:
|
| 662 |
+
"""Choose one of 2-20 distinct options for a question about a state. type="score" means ordered levels.
|
| 663 |
+
Returns the choice, a calibrated confidence and 'defer' (true = not reliable enough to act on)."""
|
| 664 |
+
with lock:
|
| 665 |
+
r = fdm.decide(model, tok, state, [{"question": question, "options": options, "type": type}],
|
| 666 |
+
defer_threshold=MIN_CONF)["results"][0]
|
| 667 |
+
return {k: r[k] for k in ("choice", "confidence", "defer", "probs", "expected_level") if k in r}
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
@mcp.tool()
|
| 671 |
+
def decide_many(state: str, questions: dict) -> dict:
|
| 672 |
+
"""Several typed questions about one state: {name: {"type": "choice"|"noul"|"score",
|
| 673 |
+
"instructions": str, "criteria": {key: description} | [levels]}}."""
|
| 674 |
+
with lock:
|
| 675 |
+
return fdm.decide(model, tok, state, questions, defer_threshold=MIN_CONF)["answers"]
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
if __name__ == "__main__":
|
| 679 |
+
mcp.run()
|
| 680 |
+
```
|
| 681 |
+
|
| 682 |
+
Register it in Claude Desktop's `claude_desktop_config.json`:
|
| 683 |
+
|
| 684 |
+
```json
|
| 685 |
+
{
|
| 686 |
+
"mcpServers": {
|
| 687 |
+
"lightdec": {
|
| 688 |
+
"command": "C:\\path\\to\\python.exe",
|
| 689 |
+
"args": ["C:\\path\\to\\lightdec_mcp.py"],
|
| 690 |
+
"env": { "LIGHTDEC_REVISION": "<commit>", "LIGHTDEC_VARIANT": "int8" }
|
| 691 |
+
}
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
```
|
| 695 |
+
|
| 696 |
+
### 7.6 As an HTTP microservice
|
| 697 |
+
|
| 698 |
+
`pip install fastapi uvicorn`, then save `serve_lightdec.py`:
|
| 699 |
+
|
| 700 |
+
```python
|
| 701 |
+
import threading
|
| 702 |
+
from fastapi import FastAPI, HTTPException
|
| 703 |
+
from pydantic import BaseModel
|
| 704 |
+
from lightdec import load_lightdec
|
| 705 |
+
|
| 706 |
+
fdm, model, tok = load_lightdec()
|
| 707 |
+
lock = threading.Lock()
|
| 708 |
+
app = FastAPI(title="LightDec")
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
class Decide(BaseModel):
|
| 712 |
+
state: str | dict
|
| 713 |
+
questions: dict | list
|
| 714 |
+
defer_threshold: float = 0.7
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
@app.get("/health")
|
| 718 |
+
def health():
|
| 719 |
+
return {"ok": True, "model": "LightDec", "version": model.fcfg.get("version")}
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
@app.post("/v1/decide")
|
| 723 |
+
def decide(req: Decide):
|
| 724 |
+
try:
|
| 725 |
+
with lock:
|
| 726 |
+
return fdm.decide(model, tok, req.state, req.questions, defer_threshold=req.defer_threshold)
|
| 727 |
+
except (ValueError, KeyError, TypeError) as exc:
|
| 728 |
+
raise HTTPException(422, str(exc))
|
| 729 |
+
```
|
| 730 |
+
|
| 731 |
+
Run it with `uvicorn serve_lightdec:app --port 9904 --workers 1`. Each worker holds its own copy of the model; scale out with more processes.
|
| 732 |
+
|
| 733 |
+
### 7.7 Without an LLM: a support intake step
|
| 734 |
+
|
| 735 |
+
```python
|
| 736 |
+
from lightdec import load_lightdec
|
| 737 |
+
|
| 738 |
+
fdm, model, tok = load_lightdec()
|
| 739 |
+
QUEUES = {"billing": "charges, invoices, payments, refunds", "returns": "returning or exchanging items",
|
| 740 |
+
"delivery": "shipping status, late or missing parcels", "accounts": "login, password, profile",
|
| 741 |
+
"human": "complaints or requests to speak to a person"}
|
| 742 |
+
|
| 743 |
+
def intake(message: str) -> dict:
|
| 744 |
+
a = fdm.decide(model, tok, message, {
|
| 745 |
+
"queue": {"type": "choice", "instructions": "Which team should handle this message?", "criteria": QUEUES},
|
| 746 |
+
"urgent": {"type": "noul", "instructions": "Does this need a reply within the hour?"},
|
| 747 |
+
})["answers"]
|
| 748 |
+
if a["queue"]["defer"]:
|
| 749 |
+
return {"action": "human_review", "suggestion": a["queue"]["choice"],
|
| 750 |
+
"reason": f"low confidence ({a['queue']['confidence']:.0%})"}
|
| 751 |
+
return {"action": "enqueue", "queue": a["queue"]["choice"],
|
| 752 |
+
"priority": "high" if a["urgent"]["p_true"] >= 0.5 else "normal",
|
| 753 |
+
"evidence": {"confidence": round(a["queue"]["confidence"], 3), "model": "LightDec"}}
|
| 754 |
+
```
|
| 755 |
+
|
| 756 |
+
---
|
| 757 |
+
|
| 758 |
+
## 8. Tuning for your domain
|
| 759 |
+
|
| 760 |
+
### 8.1 Change the deferral threshold
|
| 761 |
+
|
| 762 |
+
Pass `defer_threshold=` to `decide()`, or set `model.fcfg["defer_threshold"]`. This changes nothing in the model and is usually enough.
|
| 763 |
+
|
| 764 |
+
### 8.2 Recalibrate on your data
|
| 765 |
+
|
| 766 |
+
If ECE on your traffic (§6.4) is noticeably worse than 0.025, fit one extra temperature on top of the stored ones and save a recalibrated copy:
|
| 767 |
+
|
| 768 |
+
```python
|
| 769 |
+
import numpy as np, torch
|
| 770 |
+
# probs, gold: from §6.4 (probabilities already include the stored temperatures)
|
| 771 |
+
logp = [np.log(np.clip(p, 1e-12, 1)) for p in probs]
|
| 772 |
+
def nll(s):
|
| 773 |
+
return -np.mean([(lp / s)[g] - np.log(np.exp(lp / s).sum()) for lp, g in zip(logp, gold)])
|
| 774 |
+
s = min(np.linspace(0.5, 3.0, 51), key=nll)
|
| 775 |
+
print("extra temperature", s)
|
| 776 |
+
with torch.no_grad():
|
| 777 |
+
model.temperature.mul_(float(s))
|
| 778 |
+
fdm.save_falcondec(model, tok, "lightdec_recalibrated") # add int8=True for the compact variant
|
| 779 |
+
```
|
| 780 |
+
|
| 781 |
+
Fit on one labelled set and measure on another.
|
| 782 |
+
|
| 783 |
+
### 8.3 Fine-tune on your own decisions
|
| 784 |
+
|
| 785 |
+
Use the FalconDec training notebook (V2):
|
| 786 |
+
|
| 787 |
+
1. Export your logged and corrected decisions as JSONL: `{"state", "question", "options", "answer", "type"?, "task"?}`.
|
| 788 |
+
2. In cell 2, set `MODE="finetune"`, `FINETUNE_FROM="Falconsai/LightDec"`, `CUSTOM_DATA_JSONL="your_file.jsonl"`, and a preset.
|
| 789 |
+
3. Run the notebook. The version bumps automatically (1.0.0 → 1.0.1), and the report records the lineage.
|
| 790 |
+
|
| 791 |
+
Your tasks are up-weighted (`CUSTOM_WEIGHT`), while the public tasks keep the model general. Adding decisions with ISO-format dates and numeric tables is the most direct fix for the transfer weaknesses in §3. Check the regression gate (§6.5) before publishing.
|
| 792 |
+
|
| 793 |
+
### 8.4 Save and publish
|
| 794 |
+
|
| 795 |
+
```python
|
| 796 |
+
fdm.save_falcondec(model, tok, "lightdec_out") # fp16
|
| 797 |
+
fdm.save_falcondec(model, tok, "lightdec_out/compact-int8", int8=True)
|
| 798 |
+
|
| 799 |
+
from huggingface_hub import HfApi # needs a write token: huggingface-cli login
|
| 800 |
+
HfApi().upload_folder(folder_path="lightdec_out", repo_id="Falconsai/LightDec", commit_message="LightDec v1.0.x")
|
| 801 |
+
```
|
| 802 |
+
|
| 803 |
+
---
|
| 804 |
+
|
| 805 |
+
## 9. Architecture
|
| 806 |
+
|
| 807 |
+
```
|
| 808 |
+
[CLS] question [SEP] [MASK] option₁ [MASK] option₂ … [MASK] optionₖ [SEP] state [SEP]
|
| 809 |
+
│
|
| 810 |
+
Ettin-150M encoder (22 layers, hidden 768)
|
| 811 |
+
│
|
| 812 |
+
hidden state at each [MASK] + CLS context + question-type embedding
|
| 813 |
+
│
|
| 814 |
+
set transformer: 2 layers, 8 heads, no positional encoding → options attend to each other, order-equivariant
|
| 815 |
+
│
|
| 816 |
+
MLP → one logit per option → ÷ temperature[type, option-count bucket] → softmax
|
| 817 |
+
```
|
| 818 |
+
|
| 819 |
+
- **Option markers** (the approach Laya uses): every option is read at its own `[MASK]` token, so all options are scored in **one** pass.
|
| 820 |
+
- **Question first, state last**: when the input is too long, the tail of the state is truncated, never the options.
|
| 821 |
+
- **Adaptive option budget**: up to 24 tokens per option within a 192-token head budget that grows with the option count, and 2,048-token sequences above 24 options, so labels stay distinct. Above 96 options, `decide()` runs a tournament.
|
| 822 |
+
- **Typed primitives**: `noul` is rendered as a neutral two-option Yes/No choice; `score` keeps its level order and reports an expected level.
|
| 823 |
+
- **Calibration** lives in the model: a 3 × 4 temperature table (question type × option-count bucket: ≤2, 3–5, 6–12, >12).
|
| 824 |
+
- **int8 storage**: per-output-channel symmetric int8 for every weight matrix, fp16 elsewhere, dequantised on load.
|
| 825 |
+
|
| 826 |
+
---
|
| 827 |
+
|
| 828 |
+
## 10. Training data
|
| 829 |
+
|
| 830 |
+
**155,747 training, 13,148 validation and 17,498 test decisions** from 58 tasks. No source failed to load (`skipped_builders` is empty). Every example is a **decision**: `state`, `question`, `options`, `answer`, plus type, task and domain.
|
| 831 |
+
|
| 832 |
+
| Domain | Sources | Decisions built |
|
| 833 |
+
|---|---|---|
|
| 834 |
+
| Support | Bitext customer support | Intent routing and category routing; 30% of messages wrapped as JSON program state |
|
| 835 |
+
| Intents | CLINC150, MASSIVE (en), Banking77 *(held out)* | Intents as runtime-defined options, 2–48 per question; Banking77 also as one 77-option question |
|
| 836 |
+
| Code | CodeXGLUE code-to-text (6 languages), Devign, BigCloneBench, MBPP, HumanEval *(held out)* | Language ID, code↔description, function naming, vulnerability, clones, task→solution; bug spotting against single-fault mutants **verified to fail the unit tests** |
|
| 837 |
+
| Guardrails | Jailbreak classification, Civil Comments; deepset prompt-injections and AgentHarm *(held out)* | `noul` detection and refusal |
|
| 838 |
+
| Agentic | Counsel (human meta-evaluations of agent-step critiques), HotpotQA, AgentTrek | Step-error and critique-quality, retrieval routing and comparison yes/no; AgentTrek next-action type and finish-now. AgentTrek loaded without errors, but none of its decisions appear in the test split, so its contribution isn't measured |
|
| 839 |
+
| Workflows | LocalLLaMA/typed-decisions | `choice`/`noul`/`score` with the teacher's soft probabilities; test = the benchmark's 2,000-decision test split |
|
| 840 |
+
| Policy | Synthetic, executable (in-notebook) | Return windows, approval tiers, AND/OR eligibility, overdue invoices, table look-ups, counting, SLA urgency, access control, invoice totals; the test split uses **transfer** wording, currencies and date formats |
|
| 841 |
+
| Reasoning | ARC-Easy/Challenge and MMLU *(held out)*, OpenBookQA, SciQ, CommonsenseQA, QASC, HellaSwag, WinoGrande, MMLU auxiliary-train, BoolQ, GSM8K, AQuA-RAT, SNLI, MultiNLI, ANLI, SciTail | Multiple choice, yes/no, NLI, numeric answers with near-miss distractors |
|
| 842 |
+
| Classification | AG News, Yelp (ordinal); DAIR Emotion and SST-5 *(held out)* | Topic; 5-level `score` sentiment |
|
| 843 |
+
|
| 844 |
+
**Augmentation**: options are reshuffled every epoch (ordinal levels keep their order). In 8% of choice questions the gold answer is removed and "None of the above" becomes correct; in another 4% it is added as a distractor. **Balance**: tasks are sampled with p ∝ n^0.5 each epoch. **Leak guard**: training decisions whose (state, question) appears in validation or test were removed. Mind2Web was excluded (opt-in in the notebook). Held-out sources were never used for training, calibration or model selection.
|
| 845 |
+
|
| 846 |
+
Check each dataset's card for its license before redistributing derived data. AgentHarm is used only as a held-out evaluation, in line with its intended use.
|
| 847 |
+
|
| 848 |
+
---
|
| 849 |
+
|
| 850 |
+
## 11. Training procedure and calibration
|
| 851 |
+
|
| 852 |
+
### 11.1 Setup
|
| 853 |
+
|
| 854 |
+
| Setting | Value |
|
| 855 |
+
|---|---|
|
| 856 |
+
| Mode / preset | `scratch` from the pretrained backbone / `standard` |
|
| 857 |
+
| Data caps | ≤4,000 train, ≤300 validation, ≤300 test decisions per source split |
|
| 858 |
+
| Epochs | 2 (best: epoch 2) |
|
| 859 |
+
| Objective | Strictly proper scoring rules: log score + 0.5 × spherical score, + 1.0 × ranked probability score for `score` questions; soft targets (50/50 with the hard label) where a teacher distribution exists. These are the RLCD rewards, optimised with exact gradients |
|
| 860 |
+
| Optional RLCD stage | Off |
|
| 861 |
+
| Optimiser | AdamW (β 0.9/0.98, weight decay 0.01), encoder LR 4e-5 with layer-wise decay 0.9, head LR 3e-4, 6% warm-up, cosine decay, gradient clipping 1.0 |
|
| 862 |
+
| Batching | Token-budget batches (16,384 tokens, ≤32 decisions), length-bucketed |
|
| 863 |
+
| Weights kept | EMA of the weights (decay 0.999), best validation task-macro accuracy |
|
| 864 |
+
| Precision | bf16 autocast, TF32 matmuls; attention `auto` (FlashAttention-2 if installed, else SDPA) |
|
| 865 |
+
| Sequence | 512 tokens; 2,048 above 24 options; question ≤96 tokens; ≤24 tokens per option (more for code options) |
|
| 866 |
+
| Hardware / time | NVIDIA GeForce RTX 5090 Laptop GPU · **46 minutes** |
|
| 867 |
+
| Software | Python 3.14.4 · PyTorch 2.11.0+cu128 · transformers 5.17.0 |
|
| 868 |
+
| Seed | 42 |
|
| 869 |
+
|
| 870 |
+
| Epoch | Train loss | Train acc | Val macro | Val micro | Val NLL |
|
| 871 |
+
|---|---|---|---|---|---|
|
| 872 |
+
| 1 | 0.868 | 0.654 | 0.731 | 0.738 | 0.565 |
|
| 873 |
+
| 2 | 0.540 | 0.802 | **0.763** | 0.775 | 0.520 |
|
| 874 |
+
|
| 875 |
+
Validation was still improving at epoch 2, so a longer schedule (the `full` preset) is likely to help.
|
| 876 |
+
|
| 877 |
+
### 11.2 Fitted temperatures
|
| 878 |
+
|
| 879 |
+
| Question type | k ≤ 2 | k 3–5 | k 6–12 | k > 12 |
|
| 880 |
+
|---|---|---|---|---|
|
| 881 |
+
| choice | 1.707 | 1.352 | 1.466 | 1.349 |
|
| 882 |
+
| noul | 1.402 | 1.402 | 1.402 | 1.402 |
|
| 883 |
+
| score | 1.453 | 1.453 | 1.453 | 1.453 |
|
| 884 |
+
|
| 885 |
+
All temperatures are above 1, so the raw model was over-confident, as Laya's checkpoints are. `noul` and `score` use a single per-type temperature because their questions fall into one option-count bucket (2 options for `noul`, mostly 3–5 levels for `score`).
|
| 886 |
+
|
| 887 |
+
---
|
| 888 |
+
|
| 889 |
+
## 12. Operational notes
|
| 890 |
+
|
| 891 |
+
- **Concurrency.** `decide()` isn't internally locked. Serialise calls with a lock per process (as in §7.4–7.6) and scale out with processes.
|
| 892 |
+
- **Hardware.** On a GPU, expect tens of milliseconds per call; measure yours with §6.3. On CPU, use the int8 variant (loaded as fp32) and optionally dynamic int8 quantisation.
|
| 893 |
+
- **Input length.** 512 tokens by default. The question and options come first, so an over-long state loses its *end*. Put the decisive information early, or summarise.
|
| 894 |
+
- **Determinism.** Repeated identical calls give identical results on the same hardware and library versions. Under bf16 on GPU, batching different questions together changes padding and can move probabilities slightly (about 1e-2); near-ties can flip. Decisions with a clear margin don't change.
|
| 895 |
+
- **Traceability.** Log the model version, the Hub revision and the variant (fp16/int8) with every decision.
|
| 896 |
+
- **Offline use.** After the first download, set `HF_HUB_OFFLINE=1`, or save a local copy and load it by path.
|
| 897 |
+
|
| 898 |
+
---
|
| 899 |
+
|
| 900 |
+
## 13. Bias, risks and limitations
|
| 901 |
+
|
| 902 |
+
- **Quantitative and date reasoning.** Arithmetic (AQuA 0.259), table comparisons (0.290) and counting or summing (0.523) are near chance. Overdue-invoice checks with ISO dates score 0.407 with ECE 0.364, meaning confidently wrong.
|
| 903 |
+
- **Wide option sets.** Accuracy falls to 0.470 with all 77 Banking77 intents in one question; shortlist first.
|
| 904 |
+
- **Generalisation gap.** Held-out tasks average 0.567 against 0.725 overall. Expect lower accuracy on traffic unlike the training mix, and measure it (§6.4).
|
| 905 |
+
- **Guardrail transfer.** In-distribution jailbreak detection is 0.966, but held-out prompt-injection (0.647) and AgentHarm refusal (0.654) are much lower, with ECE 0.272 and 0.178. Don't rely on it as the only safety layer.
|
| 906 |
+
- **No completed head-to-head with proof_v2.** The comparisons in §2.4–2.5 use published numbers on different samples.
|
| 907 |
+
- **Closed world.** The model always picks one of your options. Add "None of the above" when appropriate.
|
| 908 |
+
- **Distribution-dependent calibration.** ECE was measured on this test mix; re-check it on your traffic and recalibrate if needed (§8.2).
|
| 909 |
+
- **Adversarial input.** The state is untrusted text. The model can't be instructed like an LLM, but crafted input can shift its scores. Don't make it the only safeguard before irreversible actions.
|
| 910 |
+
- **Data provenance.** Training data is English, largely crowd-sourced, templated, synthetic or scraped from public code and web tasks. Biases in these sources and in the backbone's pre-training can carry into decisions. Automated routing can systematically misroute users whose phrasing differs from the training data (dialects, non-native speakers, assistive phrasing); monitor misroutes by group where possible.
|
| 911 |
+
- **Oversight.** Not for high-stakes decisions without human review.
|
| 912 |
+
|
| 913 |
+
---
|
| 914 |
+
|
| 915 |
+
## 14. Versioning and lineage
|
| 916 |
+
|
| 917 |
+
| | proof_V_1 | proof_v2 | proof_v3 | **LightDec** |
|
| 918 |
+
|---|---|---|---|---|
|
| 919 |
+
| Model | falconsproof v1 | falconsproof v2.0.0 | FalconDec v1.0.0 (notebook V1) | **FalconDec v1.0.0 (notebook V2)** |
|
| 920 |
+
| Backbone | DistilBERT, 128 tokens | ModernBERT-base, 384 tokens | Ettin-150M, 512 / 2,048 | Ettin-150M, 512 / 2,048 |
|
| 921 |
+
| Preset / epochs | — | small / 1 per stage | small / 1 | **standard / 2** |
|
| 922 |
+
| Training decisions | — | 18,795 | ≤500 per task | **155,747** |
|
| 923 |
+
| Agentic data | — | — | — | AgentTrek, Counsel, HotpotQA |
|
| 924 |
+
| Test accuracy | — | 0.813 (23-task, code-heavy mix) | 0.518 micro / 0.535 macro | **0.725 / 0.725** (58-task mix) |
|
| 925 |
+
| ECE | — | 0.008 | 0.014 | 0.025 |
|
| 926 |
+
| Weights | — | ≈596 MB fp32 | 319 MB fp16 | 319 MB fp16 · 161 MB int8 |
|
| 927 |
+
|
| 928 |
+
LightDec is a fresh `scratch` run from the pretrained Ettin backbone (lineage: `jhu-clsp/ettin-encoder-150m`). It doesn't inherit proof_v3's or proof_v2's weights. Fine-tuning LightDec with the notebook bumps the patch version (1.0.0 → 1.0.1).
|
| 929 |
+
|
| 930 |
+
**Changelog.** LightDec 1.0.0: first release. Notebook V2, `standard` preset, 2 epochs, seed 42.
|
| 931 |
+
|
| 932 |
+
---
|
| 933 |
+
|
| 934 |
+
## 15. API reference
|
| 935 |
+
|
| 936 |
+
All functions live in `falcondec_modeling.py`.
|
| 937 |
+
|
| 938 |
+
| Function | Description |
|
| 939 |
+
|---|---|
|
| 940 |
+
| `load_falcondec(path, device=None, dtype=None, attn_implementation="sdpa")` | Loads a FalconDec directory (fp16 or int8) or Hub repo id. Returns `(model, tokenizer)`; cuda if available |
|
| 941 |
+
| `decide(model, tok, state, questions, defer_threshold=None, batch_size=32)` | Typed questions about one state. `questions` is a list or a `{key: question}` dict. Returns `{"results": [...], "answers": {key: result}}` |
|
| 942 |
+
| `score_items(model, tok, items, batch_size=32)` | Batch scoring. `items = [{"state", "question", "options", "type"?, "option_tokens"?, "seq_len"?}]`. Returns a calibrated probability array per item |
|
| 943 |
+
| `save_falcondec(model, tok, out_dir, int8=False, extra_files=None)` | Writes a self-contained directory (copies the modeling file) |
|
| 944 |
+
| `quantize_int8(state_dict)` / `dequantize_int8(state_dict)` | The per-channel int8 codec used for `compact-int8/` |
|
| 945 |
+
| `assemble(...)`, `collate_features(...)` | Low-level sequence building and batching |
|
| 946 |
+
|
| 947 |
+
**Question fields**: `type` (`choice` / `noul` / `score`, default `choice`); `question` or `instructions`; `options` (list) or `criteria` (dict `{key: description}` for choice, list of levels for score); `labels` (`{"true": …, "false": …}` wording for noul); `option_tokens` and `seq_len` (optional per-question budgets).
|
| 948 |
+
|
| 949 |
+
**Model attributes**: `model.fcfg` (the live config: layout, `special`, `defer_threshold`, `version`, `lineage`); `model.temperature` (3 × 4 tensor); `model.num_parameters()`; `model.device`.
|
| 950 |
+
|
| 951 |
+
---
|
| 952 |
+
|
| 953 |
+
## 16. Citation and references
|
| 954 |
+
|
| 955 |
+
```bibtex
|
| 956 |
+
@misc{falconsai_lightdec_2026,
|
| 957 |
+
title = {LightDec: a lightweight, single-pass, typed, calibrated decision model for agentic systems},
|
| 958 |
+
author = {{Falconsai}},
|
| 959 |
+
year = {2026},
|
| 960 |
+
howpublished = {\url{https://huggingface.co/Falconsai/LightDec}},
|
| 961 |
+
note = {FalconDec architecture, Ettin-150M backbone; successor to Falconsai/proof_v3}
|
| 962 |
+
}
|
| 963 |
+
```
|
| 964 |
+
|
| 965 |
+
**Methods.** Warner et al. (2024), *ModernBERT*. Weller et al. (2025), *Ettin* encoders. Gneiting and Raftery (2007), *Strictly Proper Scoring Rules*. Guo et al. (2017), *On Calibration of Modern Neural Networks*. Geifman and El-Yaniv (2017), *Selective Classification*. Zaheer et al. (2017), *Deep Sets*; Lee et al. (2019), *Set Transformer*. Williams (1992), REINFORCE; Shao et al. (2024), GRPO. Hinton, Vinyals and Dean (2015), *Distillation*. Related decision models: Laya (Convai Innovations), TypeSafe Jev, Together Tev1.
|
| 966 |
+
|
| 967 |
+
**Data.** ARC, OpenBookQA, SciQ, CommonsenseQA, QASC, HellaSwag, WinoGrande, MMLU, BoolQ, GSM8K, AQuA-RAT, SNLI, MultiNLI, ANLI, SciTail, CLINC150, MASSIVE, Bitext, Banking77, AG News, Yelp, DAIR Emotion, SST-5, jailbreak-classification, Civil Comments, deepset prompt-injections, AgentHarm, CodeXGLUE, MBPP, HumanEval, LocalLLaMA/typed-decisions, AgentTrek, Counsel, HotpotQA.
|
| 968 |
+
|
| 969 |
+
Report issues, misroutes or evaluation results through the Community tab of this repository.
|