What this page teaches. Decider is an AI model. It does not write text. You give it a text, a question and some options. It gives every option a probability. A probability is a share of 1.
What you will do. You run thirteen short Python files. The first asks one question. The second gets the same answer with no library. So you see how the model works. Then you ask the three kinds of question. Then you call the model in three ways. Then you watch it fail three times, and you fix each one.
What you need. Python 3.11 or newer. A computer with 16 GB of memory. 5 GB of free disk space. You need no GPU, no key and no account. You should know Python lists and dictionaries.
| A chat model | Decider | |
|---|---|---|
| You send | a prompt | a text, a question and its options |
| You get back | a sentence | a probability for every option |
| Your program then | has to read the sentence | reads a key in a dictionary |
| It can answer | anything | only one of your options |
| How sure it is | a word it writes, like "probably" | the probability itself |
💻 GET IT
python -m pip install decider-ai
# The model itself comes with the first run: 3.8 GB, from Hugging Face.
# You do not need an account there.
The model's full name is decider-2b. Mapika published it on Hugging Face. It is free: the licence is Apache-2.0.
It began as a small language model, Qwen3.5-2B-Base. That model has 1.9 billion parameters. Then it was trained on about 95 public sets of decisions.
Its makers call it "a language model that does not generate text". System One is the name of this kind of model. Decider is an open copy. The paid one is Jev, by TypeSafe AI. This site has a page about Jev.
🔍 Tutorial 1 — How it works
Two files. The first asks one question with the library. The second gets the same numbers with no library. After that, nothing about Decider is hidden.
1One question, three options
A guest at a guesthouse sends a message. Which team should get it? That is the question in every file on this page.
Decider is a class. You make one object from it, with the model's name.
decide() takes a text and a list of questions. Every question has its options.
It gives back a list. There is one answer for each question.
📄 NEW FILE · first.py
"""first.py - load the model, ask one question, and print what comes back.
pip install decider-ai
python first.py
The model runs on this computer. The first run downloads it: 3.8 GB.
"""
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
model = Decider("Mapika/decider-2b")
answers = model.decide(MESSAGE, [
{"question": "Which team should handle this message?",
"options": ["housekeeping", "maintenance", "front desk"]},
])
answer = answers[0] # decide() gives back a list: one answer for each question
print("choice:", answer["choice"])
print("confidence:", round(answer["confidence"], 4))
for option, probability in answer["probs"].items():
print(f" {probability:.4f} {option}")
🖥️ WHAT IT PRINTED
choice: housekeeping
confidence: 0.4887
0.4887 housekeeping
0.3596 maintenance
0.1516 front desk
| Line | What it does |
|---|---|
| from decider.infer import Decider | The class comes from the decider-ai library. |
| model = Decider("Mapika/decider-2b") | This loads the model. The first run downloads it. |
| model.decide(MESSAGE, [ ... ]) | One text, and a list of questions about it. |
| answer = answers[0] | The answer to the first question. Here it is the only one. |
| answer["probs"] | A dictionary. Each option has its probability. |
The first run is slow. It downloads the model, and that is 3.8 GB. Here a later run took under half a minute. Most of that time loads the model.
You will see two warnings before the answer. One says you have no Hugging Face token. You do not need one. The other says a fast path is missing. The model still works, only slower.
- Change
MESSAGEto"Can we have two more towels, please?". Run the file again. The choice is housekeeping, at 0.78. An easy message works with bare names.
2The same answer, by hand
A language model reads text as tokens. A token is a word, or a piece of a word.
For every token it knows, the model gives a number. That number is a logit. A high logit means: this token probably comes next.
A chat model picks a token, adds it, and runs again. It does that for every word it writes.
Decider runs once. Its text ends with Answer: (. The next token would be a letter. So the logits of A, B and C are the answer.
This file does it with the transformers library. It does not use the Decider class.
📄 NEW FILE · by_hand.py
"""by_hand.py - the same answer without the decider library: one pass, three letters.
python by_hand.py
It writes the text the model reads, runs the model once, and looks at the
numbers the model gives the letters A, B and C. No text is generated.
"""
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
OPTIONS = ["housekeeping", "maintenance", "front desk"]
# The model's setting for a choice question. It is in its decider_config.json.
TEMPERATURE = 1.164
# 1. The text the model reads. It stops where the letter of the answer would come.
prompt = "Context:\n" + MESSAGE
prompt += "\n\nQuestion: Which team should handle this message?\nOptions:"
for letter, option in zip("ABC", OPTIONS):
prompt += f"\n({letter}) {option}"
prompt += "\nAnswer: ("
print(prompt)
# 2. One pass. For every token it knows, the model gives a logit:
# a number that is high when that token is likely to come next.
# float32 is the kind of number the decider library uses on a CPU.
tokenizer = AutoTokenizer.from_pretrained("Mapika/decider-2b")
model = AutoModelForCausalLM.from_pretrained("Mapika/decider-2b", dtype=torch.float32)
tokens = tokenizer(prompt, return_tensors="pt")
with torch.no_grad():
next_logits = model(**tokens).logits[0, -1]
top_ids = torch.topk(next_logits, 5).indices.tolist()
read = len(tokens["input_ids"][0])
print()
print(read, "tokens were read;", len(next_logits), "tokens have a logit")
print("the five highest:", [tokenizer.decode([token_id]) for token_id in top_ids])
# 3. Keep the logits of A, B and C. Divide by the temperature. Turn them into shares of 1.
letter_ids = [tokenizer.encode(letter, add_special_tokens=False)[0] for letter in "ABC"]
logits = next_logits[letter_ids]
probabilities = torch.softmax(logits / TEMPERATURE, dim=-1)
rows = zip("ABC", OPTIONS, logits.tolist(), probabilities.tolist())
for letter, option, logit, probability in rows:
print(f"({letter}) logit {logit:.4f} probability {probability:.4f} {option}")
🖥️ WHAT IT PRINTED · the text the model read, then the numbers
Context:
The air conditioner in room 12 has been dripping water onto the bed since last night. Please send someone before 2 pm. I fly home tomorrow.
Question: Which team should handle this message?
Options:
(A) housekeeping
(B) maintenance
(C) front desk
Answer: (
66 tokens were read; 248320 tokens have a logit
the five highest: ['A', 'B', 'C', 'D', 'F']
(A) logit 13.5235 probability 0.4887 housekeeping
(B) logit 13.1665 probability 0.3596 maintenance
(C) logit 12.1614 probability 0.1516 front desk
first.py printed. So this is all that decide() does. Look at the five highest tokens too. They are all letters.
- 1 · The textthe message, the question, the options, then
Answer: ( - 2 · One passthe model reads all the tokens once
- 3 · Three logitsthe numbers for A, B and C
- 4 · Divideeach one by the temperature
- 5 · Softmaxthe three become shares of 1
Try it: from logits to probabilities
These are the logits that by_hand.py printed. Move one of them. Or move the temperature. A high temperature makes the three more equal. A low one gives almost everything to the highest.
| Line | What it does |
|---|---|
| prompt += "\nAnswer: (" | The text stops here. The next token would be the letter. |
| model(**tokens).logits[0, -1] | All the logits after the last token. 0 is the first text. -1 is its last token. |
| tokenizer.encode(letter, ...)[0] | The number of the token for one letter. |
| logits = next_logits[letter_ids] | Keep three logits. The other 248,317 are not used. |
| torch.softmax(logits / TEMPERATURE, dim=-1) | Divide, then turn the three numbers into shares of 1. |
decider_config.json.
The temperature never changes which option is highest.
The library is faster than this file. This file works out 248,320 logits and keeps three. The library works out the letters' logits only.
- Set
TEMPERATURE = 1. Run the file. The three probabilities are now 0.5113, 0.3578 and 0.1309. The highest one got higher. - Set
TEMPERATURE = 0.1. Now housekeeping gets 0.9726. The order of the three is still the same.
🧩 Tutorial 2 — Three kinds of question
system_one() is the fuller way to ask. Each question has a type. There are three types.
Jev takes questions in the same form. That is where the names come from.
| Type | It asks | You write | The answer |
|---|---|---|---|
choice | Which one of these? | a name and a meaning for each option | the name, and a probability for each option |
noul | Yes or no? | the question | one number: the probability of yes |
score | Where on a scale? | the levels, from low to high | the expected level, and a probability for each level |
3choice: one of several
A choice question has instructions and criteria. The instructions are the question. The criteria are the options.
Each option is a name and its meaning. The meaning can be None. Then the model sees the name alone.
You can give from 2 to 255 options.
The questions go in a dictionary. Its key, here team, is your name for the question. The model never sees it.
📄 NEW FILE · choice.py
"""choice.py - a choice question: one of several options, each with its meaning.
python choice.py
"""
import json
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
model = Decider("Mapika/decider-2b")
question = {
"type": "choice",
"instructions": "Which team should handle this message?",
"criteria": { # option name: what the option means
"housekeeping": "Cleaning, towels, bed linen",
"maintenance": "Repairs: air conditioner, water, electricity",
"front_desk": "Bookings, check-out, bills, questions",
"other": None, # None: the name alone
},
}
result = model.system_one(MESSAGE, {"team": question})
print(json.dumps(result, indent=2))
🖥️ WHAT IT PRINTED
{
"model": "decider-2b-v11",
"answers": {
"team": {
"type": "choice",
"choice": "maintenance",
"confidence": 0.8022,
"x_p_max": 0.8516,
"certainty": 0.6104,
"probabilities": {
"housekeeping": 0.1023,
"maintenance": 0.8516,
"front_desk": 0.0321,
"other": 0.014
}
}
},
"usage": {
"input_tokens": 98,
"output_tokens": 0
}
}
| Key | What it is | Here |
|---|---|---|
choice | The option with the highest probability. | maintenance |
probabilities | One for each option. They add up to 1. | 0.1023, 0.8516, 0.0321 and 0.014 |
x_p_max | The highest probability. | 0.8516 |
confidence | The highest probability on another scale. It is 0 when all options are equal. It is 1 when one option has everything. With n options: (n × x_p_max − 1) ÷ (n − 1). | (4 × 0.8516 − 1) ÷ 3 = 0.802 |
certainty | How unequal the probabilities are. It is 0 when all are equal. It is 1 when one option has everything. It is 1 minus the entropy, divided by ln(n). | 0.6104 |
usage | The tokens the model read. It never writes any. | 98 read, 0 written |
system_one() it is the number from the table above.
In decide() it is the highest probability itself. Step 1 printed that one.
- Change the three meanings to
None. Run the file. The choice is housekeeping again, at 0.50. So the meanings did the work.
4noul: yes or no
noul is the name of the yes/no type. Its answer is one number.
The number is the probability of yes. Near 0 means no. Near 1 means yes.
You can write the question in two ways. The first is a sentence, in instructions. The second is the meaning of true and of false, in criteria.
📄 NEW FILE · noul.py
"""noul.py - a yes/no question. The answer is one number: how much "yes".
python noul.py
"""
import json
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
model = Decider("Mapika/decider-2b")
questions = {
# the usual form: the question as a sentence
"refund": {"type": "noul", "instructions": "Does the guest ask for money back?"},
# the other form: no sentence, but what "yes" and "no" mean
"today": {"type": "noul", "criteria": {"true": "The guest needs help today",
"false": "It can wait"}},
}
result = model.system_one(MESSAGE, questions)
print(json.dumps(result["answers"], indent=2))
🖥️ WHAT IT PRINTED
{
"refund": {
"type": "noul",
"noul": 0.0304
},
"today": {
"type": "noul",
"noul": 0.8425
}
}
| Line | What the model reads |
|---|---|
| {"type": "noul", "instructions": "Does the guest ask for money back?"} | The question, and two options: (A) no and (B) yes. So a noul is a choice with two options. |
| "criteria": {"true": "The guest needs help today", "false": "It can wait"} | The options are now no: It can wait and yes: The guest needs help today. The question is a fixed one: "Which answer fits the context?" |
if answer["noul"] > 0.5: is the usual line.
Ask for more, like 0.9, before an action that costs money.
- Add one sentence to the end of
MESSAGE:I want my money back for last night.Run the file.refundrises from 0.03 to 0.88.
5score: where on a scale
A score question has a list of levels. You write them from low to high. The first one is level 0.
You can give from 2 to 10 levels.
The answer's score is the expected level. It is each level times its probability, added up.
📄 NEW FILE · score.py
"""score.py - a score question: where on a scale? The levels are written out, low to high.
python score.py
"""
import json
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
model = Decider("Mapika/decider-2b")
question = {
"type": "score",
"instructions": "How soon does the guesthouse have to act?",
"criteria": [ # level 0, level 1, level 2
"It can wait a few days",
"It should be done today",
"It must be done within a few hours",
],
}
result = model.system_one(MESSAGE, {"urgent": question})
print(json.dumps(result["answers"], indent=2))
🖥️ WHAT IT PRINTED
{
"urgent": {
"type": "score",
"score": 1.57,
"confidence": 0.3618,
"x_p_max": 0.5819,
"certainty": 0.3474,
"legend": {
"0": "It can wait a few days",
"1": "It should be done today",
"2": "It must be done within a few hours"
},
"probabilities": {
"0": 0.0074,
"1": 0.4106,
"2": 0.5819
},
"level_fit": {
"0": 0.0063,
"1": 0.3473,
"2": 0.4922
},
"fit_mass": 0.8457
}
}
level_fit holds the three yes numbers.
fit_mass is their sum. Each probability is a fit divided by that sum.
A fit_mass near 1 means one level fits. A low one means none fits. A high one means several fit.
| Key | What it is | Here |
|---|---|---|
score | The expected level. | 1.57 |
level_fit | For each level alone: the probability that it fits. | 0.0063, 0.3473 and 0.4922 |
fit_mass | The sum of the three fits. | 0.8457 |
probabilities | Each fit divided by fit_mass. They add up to 1. | 0.4922 ÷ 0.8457 = 0.582 |
confidence | It is 1 when one level has everything. It falls when other levels get a share. Far levels cost more than near ones. | 0.3618 |
legend | Your levels, with their numbers. | 0, 1 and 2 |
- Change
MESSAGEto"Could we have a late check-out next Friday?". Run the file. The score is 0.97. That looks like "today". It is not. - Read the other keys of that answer.
fit_massis 0.47: no level fits well. The three probabilities are almost equal.confidenceis 0.0. So check those two keys before you use a score.
6A record, and three questions in one call
The text you give is called the state. It can be a string, a dictionary or a list.
A question can name one part of the state. Write the path to that part between backticks.
One call can hold many questions. Each question gets its own pass. So an answer does not depend on the other questions.
📄 NEW FILE · one_call.py
"""one_call.py - a record as the state, and three questions in one call.
python one_call.py
"""
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
# The state does not have to be a sentence. A dictionary or a list works too.
BOOKING = {
"guest": {"name": "Anna", "room": 12, "nights": 3},
"messages": [{"from": "guest", "text": MESSAGE}],
}
# A question can name one part of the state by its path, between backticks.
QUESTIONS = {
"team": {
"type": "choice",
"instructions": "Which team should handle `messages[0].text`?",
"criteria": {
"housekeeping": "Cleaning, towels, bed linen",
"maintenance": "Repairs: air conditioner, water, electricity",
"front_desk": "Bookings, check-out, bills, questions",
"other": None,
},
},
"refund": {
"type": "noul",
"instructions": "Does `messages[0].text` ask for money back?",
},
"urgent": {
"type": "score",
"instructions": "How soon does the guesthouse have to act?",
"criteria": ["It can wait a few days", "It should be done today",
"It must be done within a few hours"],
},
}
model = Decider("Mapika/decider-2b")
result = model.system_one(BOOKING, QUESTIONS)
for name, answer in result["answers"].items():
kind = answer["type"] # "choice", "noul" or "score"
# The answer itself is under the key that has the name of its type.
print(f"{name:7} {kind:7} {answer[kind]}")
print(result["usage"])
🖥️ WHAT IT PRINTED
team choice maintenance
refund noul 0.0232
urgent score 1.5
{'input_tokens': 299, 'output_tokens': 0}
usage counts the state once: 299 tokens in all.
- Delete the
urgentquestion. Run the file. The other two answers do not change: maintenance and 0.0232. Each question has its own pass.
🔌 Tutorial 3 — Three ways to call it
The class has three ways in. decide() is the plain one, from step 1. system_one() is the full one, from steps 3 to 6. decide_json() sits between them.
First you ask the library itself what it takes. Then two files show decide_json(), and decide() for many texts.
7Ask the library what it takes
You do not have to remember the three ways. Python can show them to you.
inspect.signature() prints what a function takes. Give it the function's name, with no brackets after the name.
This library has no type hints. So the kinds of question are written in a docstring. The file prints that too.
This file loads no model. It runs in a few seconds.
📄 NEW FILE · look_inside.py
"""look_inside.py - ask the library what its functions take. No model is loaded.
python look_inside.py
"""
import inspect
import decider.systemone
from decider.infer import Decider
# A name with no brackets after it is the function itself, and Python can look at it.
# With brackets, Decider.system_one(), Python would call it.
print("decide", inspect.signature(Decider.decide))
print("decide_batch", inspect.signature(Decider.decide_batch))
print("decide_json", inspect.signature(Decider.decide_json))
print("system_one", inspect.signature(Decider.system_one))
print()
# The library has no type hints. What a question looks like is written in this docstring.
print(decider.systemone.__doc__)
🖥️ WHAT IT PRINTED
decide (self, context, questions, **kw)
decide_batch (self, requests, max_ctx_tokens=1536)
decide_json (self, context, schema, **kw)
system_one (self, state, questions, independent=True, max_state_tokens=32768, max_fwd_tokens=65536, layout=None, isolated=None)
Jev-shaped requests on top of the decider prompt format (same wire format as TypeSafe's POST /v1/systemone).
state str | dict | list JSON state is serialised compactly; questions may name a part by path (`ticket.messages[0].text`)
questions {id: {"type": "choice", "instructions": ..., "criteria": {name: description | {...} | [...] | None}} up to 255 options
{"type": "score", "instructions": ..., "criteria": [level 0 description, level 1 description, ...]} 2..10 levels
{"type": "noul", "instructions": ... (optional), "criteria": {"true": ..., "false": ...} (optional)}}
a noul question needs instructions or at least one true/false description.
ids are never shown to the model. `instructions` and every description may be a string or any JSON value.
| In the output | What it means |
|---|---|
| self | The model object. Python fills it in. You do not pass it. |
| independent=True | A value after = is a default. You can leave that setting out. |
| max_ctx_tokens=1536 | The most tokens of text that decide_batch() reads. Step 12 is about this number. |
| **kw | decide() passes any other setting on to decide_batch(). |
| str | dict | list | The state can be a string, a dictionary or a list. |
model.system_one is the function. Python can look at it.
model.system_one() calls the function. That fails: the state and the questions are missing.
So dir(model.system_one()) gives an error. inspect.signature(model.system_one) works.
- Add one line at the end:
print(inspect.signature(Decider)). Run the file. It prints what you can set when you make the model object.abstain_belowanduse_graphsare in that list.
8decide_json(): the question is the key
You write one dictionary. Each key is a question. Each value says what kind of answer you want.
The kinds are bool, choice and scale. They match noul, choice and score.
An option has no separate meaning here. So put the name and the meaning in one string.
📄 NEW FILE · fields.py
"""fields.py - decide_json(): the question is the key, its field is the kind of answer.
python fields.py
"""
import json
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
FIELDS = {
"Which team should handle this message?": {
"type": "choice",
"options": ["housekeeping: cleaning, towels, bed linen",
"maintenance: repairs, air conditioner, water, electricity",
"front desk: bookings, check-out, bills, questions"],
},
"Does the guest ask for money back?": {"type": "bool"},
"How soon does the guesthouse have to act?": {
"type": "scale",
"legend": ["it can wait a few days", "it should be done today",
"it must be done within a few hours"],
},
}
model = Decider("Mapika/decider-2b")
answers = model.decide_json(MESSAGE, FIELDS)
print(json.dumps(answers, indent=2))
🖥️ WHAT IT PRINTED
{
"Which team should handle this message?": {
"choice": "maintenance: repairs, air conditioner, water, electricity",
"confidence": 0.8762,
"type": "choice",
"probabilities": {
"housekeeping: cleaning, towels, bed linen": 0.1014,
"maintenance: repairs, air conditioner, water, electricity": 0.8762,
"front desk: bookings, check-out, bills, questions": 0.0225
}
},
"Does the guest ask for money back?": {
"noul": 0.016,
"type": "noul"
},
"How soon does the guesthouse have to act?": {
"score": 1.89,
"confidence": 0.8973,
"type": "scale",
"legend": [
"it can wait a few days",
"it should be done today",
"it must be done within a few hours"
],
"probabilities": {
"0": 0.0034,
"1": 0.0993,
"2": 0.8973
}
}
}
confidence is the highest probability. There is no x_p_max and no certainty.
- Give the levels your own numbers. Write the legend as a dictionary. Its keys are
"0","5"and"10". Run the file. The score is now 9.15, on a scale from 0 to 10.
9decide_batch(): many texts at once
decide_batch() takes a list of requests. Each request is a text and its questions.
abstain_below is a setting of the model object. It is a probability. Below it, the choice is None.
📄 NEW FILE · many.py
"""many.py - six messages in one call, and no answer where the model is not sure.
python many.py
"""
from decider.infer import Decider
MESSAGES = [
"Can we have two more towels, please?",
"The wifi password on the card does not work.",
"What time is breakfast?",
"My bill shows three nights but I stayed two.",
"The shower has no hot water.",
"There is a gecko on my wall and I love it.",
]
QUESTION = {
"question": "Which team should handle this message?",
"options": ["housekeeping: cleaning, towels, bed linen",
"maintenance: repairs, air conditioner, water, electricity, wifi",
"front desk: bookings, bills, questions"],
}
# abstain_below: when the best option has less than this, the choice is None.
model = Decider("Mapika/decider-2b", abstain_below=0.7)
# One request for each message: the text, and the list of its questions.
results = model.decide_batch([(message, [QUESTION]) for message in MESSAGES])
for message, answers in zip(MESSAGES, results):
answer = answers[0]
team = answer["choice"].split(":")[0] if answer["choice"] else "(not sure)"
print(f"{answer['confidence']:.3f} {team:13} {message}")
🖥️ WHAT IT PRINTED
0.934 housekeeping Can we have two more towels, please?
0.888 maintenance The wifi password on the card does not work.
0.955 front desk What time is breakfast?
0.991 front desk My bill shows three nights but I stayed two.
0.932 maintenance The shower has no hot water.
0.420 (not sure) There is a gecko on my wall and I love it.
- Set
abstain_below=0.9. Run the file. Now the wifi message is(not sure)too. Its best option had 0.888.
The three, side by side
decide() | decide_json() | system_one() | |
|---|---|---|---|
| A question is | a dictionary with question and options | a key, and a field with its type | a name, and type, instructions, criteria |
| Kinds of question | a choice only | bool, choice, scale | noul, choice, score |
| The text can be | a string | a string | a string, a dictionary or a list |
| It reads at most | 1,536 tokens of the text | 1,536 tokens of the text | 32,768 tokens |
| Several questions | all in one pass | all in one pass | each in its own pass |
confidence is | the highest probability | the highest probability | 0 when all options are equal |
| Many texts in one call | decide_batch() | decide_json_batch() | no |
abstain_below | works | works for a choice | does nothing |
decide() and decide_json() put a text's questions in one pass.
A question then sees the questions before it. Their order can change an answer.
The makers measured it. A new order changed up to 12% of the answers.
system_one() gives each question its own pass. So use it when order must not matter.
Which one? Start with system_one(). It takes meanings, records and long texts. Use decide_batch() for many short texts with one question each.
More in the library. Three more things are not shown on this page:
schema()prepares a fixed set of questions once. It failed on this laptop, which has no GPU. Its makers say it costs some accuracy.decider.serveis a web server. It answers the same questions atPOST /v1/systemone.- The library can also load a GGUF file, through llama.cpp.
🪤 Tutorial 4 — Where it fails, and how to ask well
Decider is a small model, and it reads once. Three files show three ways it fails. Each file also shows the fix.
10It cannot compare numbers
A river is at 3.9 m. Its bank is 3.7 m high. Is the river higher than the bank?
That needs one comparison. The model cannot do it.
So the program compares the numbers. It writes the result as words. Then the model reads the words.
📄 NEW FILE · compare.py
"""compare.py - can the model compare two numbers? And what a program does about it.
python compare.py
Twelve river levels beside a bank of 3.7 m. First the model gets the two numbers.
Then the program compares them and writes the result as words.
"""
from decider.infer import Decider
BANK = 3.7
LEVELS = [3.2, 3.9, 2.1, 3.75, 5.3, 3.65, 4.0, 1.2, 3.71, 3.0, 6.8, 3.69]
ASK = "Is the river higher than the bank?"
QUESTION = {"over": {"type": "noul", "instructions": ASK}}
def yes(model, text):
"""How much "yes" the model gives the question for this text."""
return model.system_one(text, QUESTION)["answers"]["over"]["noul"]
def main():
model = Decider("Mapika/decider-2b")
right_from_numbers = right_from_words = 0
print("river truth numbers only with the words")
for level in LEVELS:
numbers = f"The river is at {level} m. The river bank is {BANK} m high."
# The program compares the two numbers. The model only reads the result.
compared = "higher than" if level > BANK else "lower than"
words = numbers + f" The river is {compared} the bank."
truth = level > BANK
from_numbers, from_words = yes(model, numbers), yes(model, words)
right_from_numbers += (from_numbers > 0.5) == truth
right_from_words += (from_words > 0.5) == truth
answer = "yes" if truth else "no"
print(f"{level:5} {answer:5} {from_numbers:.4f} {from_words:.4f}")
print(f"right with the numbers only: {right_from_numbers} of {len(LEVELS)}")
print(f"right with the words: {right_from_words} of {len(LEVELS)}")
if __name__ == "__main__":
main()
🖥️ WHAT IT PRINTED · the two right-hand columns are the probability of yes
river truth numbers only with the words
3.2 no 0.1014 0.0248
3.9 yes 0.2579 0.8156
2.1 no 0.0940 0.0242
3.75 yes 0.2026 0.7521
5.3 yes 0.1957 0.8179
3.65 no 0.1244 0.0255
4.0 yes 0.2168 0.8219
1.2 no 0.0875 0.0258
3.71 yes 0.1718 0.7286
3.0 no 0.1095 0.0275
6.8 yes 0.2685 0.8363
3.69 no 0.1375 0.0261
right with the numbers only: 6 of 12
right with the words: 12 of 12
- Set
BANK = 5.0. Run the file. With the numbers only, it is now right 10 times of 12. The model did not get better. It still says no every time. Now ten of the answers are no.
11Extra text moves the answer
One question: does the guest ask for money back? The guest does not.
The file asks it four times. Each time there is more text around the message.
📄 NEW FILE · too_much.py
"""too_much.py - text on the same subject changes an answer, wherever the question points.
python too_much.py
One yes/no question about a guest's message, asked with more and more around the message.
"""
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
BOOKING = {
"guest": {"name": "Anna", "room": 12, "nights": 3},
"messages": [{"from": "guest", "text": MESSAGE}],
}
POLICY = "A night in a room that cannot be slept in is refunded."
ASK = "Does the guest ask for money back?"
ASK_BY_PATH = "Does `messages[0].text` ask for money back?"
def yes(model, state, instructions):
"""How much "yes" the model gives this question about this state."""
question = {"refund": {"type": "noul", "instructions": instructions}}
return model.system_one(state, question)["answers"]["refund"]["noul"]
def main():
model = Decider("Mapika/decider-2b")
# The same record, with the hotel's rule about refunds added to it.
with_policy = dict(BOOKING, policy=POLICY)
rows = [
(MESSAGE, ASK, "the message alone"),
(BOOKING, ASK, "the booking record"),
(with_policy, ASK, "the record with the refund rule in it"),
(with_policy, ASK_BY_PATH, "the same, and the question names the message"),
]
for state, instructions, label in rows:
print(f"{yes(model, state, instructions):.4f} {label}")
if __name__ == "__main__":
main()
🖥️ WHAT IT PRINTED · the probability of yes
0.0304 the message alone
0.0196 the booking record
0.6109 the record with the refund rule in it
0.5281 the same, and the question names the message
- Change
POLICYto"Breakfast is served from 7 to 10 in the morning."Run the file. The last two numbers fall to 0.02. A rule about breakfast does not move a question about money.
12A long text is cut
decide() reads the first 1,536 tokens of the text. It drops the rest. It gives no warning.
This text has 150 house rules. The guest's message comes after them. That is 2,969 tokens. So the message is cut off.
📄 NEW FILE · long_text.py
"""long_text.py - decide() reads only the start of a long text.
python long_text.py
The text is a long list of house rules, and then the guest's message at the end.
"""
from decider.infer import Decider
from transformers import AutoTokenizer
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
RULE = "Rule {number}: the guest in room {number} returns the key before noon."
ASK = "Which team should handle the guest's message?"
TEAMS = {"housekeeping": "cleaning, towels, bed linen",
"maintenance": "repairs, air conditioner, water, electricity",
"front desk": "bookings, check-out, bills, questions"}
# decide() takes each option as one string: "name: what it is for".
QUESTION = {"question": ASK,
"options": [f"{team}: {work}" for team, work in TEAMS.items()]}
def show(label, answer):
team = answer["choice"].split(":")[0]
print(f"{answer['confidence']:.4f} {team:12} {label}")
def main():
rules = "\n".join(RULE.format(number=number) for number in range(1, 151))
text = rules + "\n\nGuest message: " + MESSAGE
tokenizer = AutoTokenizer.from_pretrained("Mapika/decider-2b")
print(len(tokenizer.encode(text)), "tokens in the text")
model = Decider("Mapika/decider-2b")
show("decide(): it reads the first 1,536 tokens",
model.decide(text, [QUESTION])[0])
show("decide() with max_ctx_tokens=8192",
model.decide(text, [QUESTION], max_ctx_tokens=8192)[0])
show("decide(), with the message first and the rules after it",
model.decide("Guest message: " + MESSAGE + "\n\n" + rules, [QUESTION])[0])
# system_one() reads up to 32,768 tokens, so it needs no extra setting here.
question = {"team": {"type": "choice", "instructions": ASK, "criteria": TEAMS}}
answer = model.system_one(text, question)["answers"]["team"]
label = "system_one(), the same long text"
print(f"{answer['x_p_max']:.4f} {answer['choice']:12} {label}")
if __name__ == "__main__":
main()
🖥️ WHAT IT PRINTED
2969 tokens in the text
0.9852 front desk decide(): it reads the first 1,536 tokens
0.6237 maintenance decide() with max_ctx_tokens=8192
0.8349 maintenance decide(), with the message first and the rules after it
0.6237 maintenance system_one(), the same long text
| The fix | How | Here |
|---|---|---|
| Raise the limit | decide(text, questions, max_ctx_tokens=8192) | maintenance, 0.62 |
| Put the main text first | the message, then the rules | maintenance, 0.83 |
Use system_one() | it reads up to 32,768 tokens | maintenance, 0.62 |
- Change
range(1, 151)torange(1, 51). Run the file. The text now has 967 tokens, so nothing is cut. All four lines say maintenance.
How to ask well
| Do this | Why | From |
|---|---|---|
| Give every option a meaning. | Bare names gave the wrong team. | steps 1 and 3 |
| Let your program do the arithmetic. | The model cannot compare two numbers. | step 10 |
| Keep the state small. | Text on the same subject moves the answer. | step 11 |
| Count the tokens of a long text. | decide() cuts it at 1,536. | step 12 |
| Ask one plain question at a time. | The model does not reason in steps. Split a hard question into small ones. | the model card |
| Do not write a rule into the question. | "Fill if empty, otherwise skip" is not followed. | the model card |
| Use it for English. | The model was trained on English. | the model card |
| Test it on your own examples. | It is too sure on hard questions. Check its numbers before your program trusts them. | the model card |
⏱️ Speed, size and the family
One last file. It measures how long the model takes on your computer.
13How fast, how big
The file asks each question three times. It prints the middle time.
The first question after loading is slower. So one is asked first, and its time is thrown away.
📄 NEW FILE · speed.py
"""speed.py - how long the model takes on this computer.
python speed.py
Each question is asked three times and the middle time is printed. The first
question after loading is slower, so one is asked and thrown away first.
"""
import time
from decider.infer import Decider
MESSAGE = ("The air conditioner in room 12 has been dripping water onto the bed "
"since last night. Please send someone before 2 pm. I fly home tomorrow.")
CHOICE = {"type": "choice", "instructions": "Which team should handle this message?",
"criteria": {"housekeeping": "Cleaning, towels, bed linen",
"maintenance": "Repairs: air conditioner, water, electricity",
"front_desk": "Bookings, check-out, bills, questions",
"other": None}}
NOUL = {"type": "noul", "instructions": "Does the guest ask for money back?"}
SCORE = {"type": "score", "instructions": "How soon does the guesthouse have to act?",
"criteria": ["It can wait a few days", "It should be done today",
"It must be done within a few hours"]}
SHORT = ["Can we have two more towels, please?", "What time is breakfast?",
"The shower has no hot water.", "The wifi password does not work.",
"My bill shows three nights but I stayed two.", "Is there a laundry service?"]
PLAIN = {"question": "Which team should handle this message?",
"options": ["housekeeping", "maintenance", "front desk"]}
def middle(times):
return sorted(times)[len(times) // 2]
def ask(model, questions):
"""Seconds for one system_one() call with these questions: the middle of three."""
times = []
for _ in range(3):
started = time.perf_counter()
model.system_one(MESSAGE, questions)
times.append(time.perf_counter() - started)
return middle(times)
def main():
started = time.perf_counter()
model = Decider("Mapika/decider-2b")
print(f"load the model {time.perf_counter() - started:5.1f} s")
model.system_one(MESSAGE, {"first": NOUL}) # thrown away
print(f"one yes/no {ask(model, {'q': NOUL}):5.1f} s")
print(f"one choice, four options {ask(model, {'q': CHOICE}):5.1f} s")
print(f"one score, three levels {ask(model, {'q': SCORE}):5.1f} s")
all_three = {"a": CHOICE, "b": NOUL, "c": SCORE}
print(f"all three in one call {ask(model, all_three):5.1f} s")
times = []
for _ in range(3):
started = time.perf_counter()
model.decide_batch([(message, [PLAIN]) for message in SHORT])
times.append(time.perf_counter() - started)
print(f"six short messages, decide_batch {middle(times):5.1f} s")
if __name__ == "__main__":
main()
🖥️ WHAT IT PRINTED · a laptop with no GPU
load the model 6.0 s
one yes/no 1.8 s
one choice, four options 3.3 s
one score, three levels 7.1 s
all three in one call 7.4 s
six short messages, decide_batch 5.0 s
🖥️ ALL THIRTEEN FILES · the seconds, and the most memory held
python 3.12.10 on Windows AMD64, 22 logical cores; decider-ai 1.8.1, torch 2.11.0, transformers 5.8.0
first.py 13.7 s 10.4 GB
by_hand.py 13.0 s 9.8 GB
choice.py 14.5 s 10.3 GB
noul.py 15.8 s 11.0 GB
score.py 16.9 s 10.3 GB
one_call.py 22.4 s 10.2 GB
look_inside.py 6.1 s 0.4 GB
fields.py 16.6 s 10.9 GB
many.py 20.7 s 10.1 GB
compare.py 42.4 s 10.7 GB
too_much.py 22.5 s 10.6 GB
long_text.py 148.4 s 10.4 GB
speed.py 89.0 s 9.6 GB
A file with the model takes most of its time to start. Python loads the libraries. Then the model loads. Only look_inside.py loads no model.
The model is 3.8 GB on disk. There, each of its numbers takes 2 bytes. In memory, on a CPU, each takes 4. So a file with the model holds about 10 GB.
| On a GPU | Time | Measured by |
|---|---|---|
| One request, with the library's fast path | 3.2 ms | the makers, on an NVIDIA B300 |
| One request, without the fast path | 18.9 ms | the makers, on an NVIDIA B300 |
| The first decision, while the fast path is built | 92 s | this site, on an NVIDIA DGX Spark |
| Each decision after that | 0.02 s | this site, on an NVIDIA DGX Spark |
Decider("Mapika/decider-2b", use_graphs=False).
How good is it? Its makers tested it on 95 public tasks. It was trained on the kind of 67 of them. There it was right 80 times in 100. The other 28 were new to it. There it was right 75 times in 100.
This site tested it too, on 112 chatbot messages in three languages. It sent 102 to the right helper. Jev sent 110. In Thai it got 28 of 37. See Free System 1 models vs Jev.
The family. Decider comes in other sizes. They all take the same questions.
| Model | Size on disk | Its makers say |
|---|---|---|
decider-0.8b | 1.4 GB | The smallest. 1.5 times faster than the 2b. |
decider-2b | 3.8 GB | The default. It is the one on this page. |
decider-4b | 8.4 GB | Better at knowledge questions and hard judgments. |
decider-35b-a3b | 65 GB | The most accurate. Each decision costs 3 to 4 times more. |
decider-2b-vision | 4.1 GB | It decides from an image and a question. |
Run it all again, and where next
Every file on this page has a Download button. Two more files check the page itself.
run_all.py runs the thirteen files. It saves what each one printed. The boxes on this page are those saved files.
your_turn.py makes every change from the Your Turn boxes. It runs each one. Its output is the third file below.
| You want | Go to |
|---|---|
| Every number its makers measured | the model card on Hugging Face |
| The code of the library | github.com/Mapika/decider |
| The paid model it copies | Jev: an answer, not a sentence |
| Decider against six other pickers | Free System 1 models vs Jev |
| Decider inside a program that acts | A gecko that hunts: Decider picks each move |