5  Decision Models

library(rtemis.llm)
  .:rtemis.llm v.0.8.8 · aarch64-apple-darwin23

A decision model answers typed questions about a passage with a probability for every option, and writes no text. There are two kinds of question:

The model cannot answer outside the options it was given, so its answers need no repair. Every answer carries a probability, which you can rank by, threshold, or average.

Ollama serves decision models locally, for example clef-flash. OpenRouter serves others, for example typesafe/jev-1.13, with an API key.

ollama_list_decision_models()
[1] "clef-flash:latest"

5.1 Create a DecisionModel

A configuration says where the model runs:

  • config_OllamaDecision() for a local model on Ollama;
  • config_OpenRouterDecision() for a model on OpenRouter. The key is read from OPENROUTER_API_KEY, api_key, or a Keychain item.
dm <- create_DecisionModel(config_OllamaDecision("clef-flash"))
dm
<DecisionModel>
        Model: clef-flash
     Provider: Ollama
     Base URL: http://localhost:11434
 

A decision model has no system prompt and no sampling options. Text that applies to every passage, such as the task or a rubric, goes in context, which is placed before each passage.

5.2 Ask questions

Questions are a decision model’s own interface. You write each question, and for a choice, what each option means. The model judges the passage against those meanings.

The examples below screen study abstracts. They ask for the study design and for whether the primary result is statistically significant.

abstracts <- c(
  trial = paste(
    "In this multicenter randomized controlled trial, 412 adults with type 2 diabetes were",
    "assigned to semaglutide or placebo for 52 weeks. HbA1c fell by 1.4 percentage points with",
    "semaglutide and by 0.2 with placebo (difference -1.2; 95% CI -1.5 to -0.9; p < 0.001)."
  ),
  cohort = paste(
    "We followed 12,840 nurses for 20 years to examine the association between rotating",
    "night-shift work and incident breast cancer. Night-shift work was associated with a slightly",
    "higher risk that was not statistically significant (HR 1.12; 95% CI 0.98 to 1.28)."
  ),
  model = paste(
    "We trained a convolutional neural network on 38,000 chest radiographs to detect",
    "pneumothorax and evaluated it on an external test set of 2,100 radiographs read by three",
    "thoracic radiologists. The model reached an AUC of 0.94 (95% CI 0.92 to 0.96)."
  )
)
qs <- list(
  design = choice(
    "Which study design does the abstract describe?",
    c(
      randomized_trial = "Participants are randomly assigned to the interventions being compared",
      cohort = "A group is followed over time to relate exposures to later outcomes",
      case_control = "People with and without an outcome are compared on past exposures",
      cross_sectional = "Exposures and outcomes are measured at a single point in time",
      diagnostic_accuracy = "A test or model is evaluated against a reference standard",
      systematic_review = "Existing studies are identified and summarized or pooled"
    )
  ),
  significant = noul("Does the abstract report a statistically significant primary result?")
)
d <- decide(dm, abstracts[["cohort"]], qs)
2026-10-04 02:07:16 [clef-flash] deciding... [decide]
2026-10-04 02:07:28 [clef-flash] done. [decide]
d
<Decision>
Model: clef-flash
     design: cohort (p = 0.957, confidence 0.863)
significant: noul 0.135

probabilities() returns every option’s probability:

probabilities(d)
      question              option           p chosen unsure
        <char>              <char>       <num> <lgcl> <lgcl>
1:      design              cohort 0.956659546   TRUE  FALSE
2:      design   systematic_review 0.012267256  FALSE  FALSE
3:      design        case_control 0.011475782  FALSE  FALSE
4:      design     cross_sectional 0.008184194  FALSE  FALSE
5:      design diagnostic_accuracy 0.005850910  FALSE  FALSE
6:      design    randomized_trial 0.005562312  FALSE  FALSE
7: significant                true 0.135298251  FALSE  FALSE
8: significant               false 0.864701749   TRUE  FALSE

unsure flags an answer whose probabilities are spread out: a choice whose confidence is below 0.5, or a noul within 0.15 of 0.5. The provider’s confidence measures how concentrated the probabilities are, not how often the pick is right.

5.3 Ask the same questions of many passages

dmapply() asks the same questions about each element. It returns one table with an index column, and the wall time of each call in seconds.

res <- dmapply(abstracts, dm, questions = qs)

⠋ [clef-flash] 1/3
⠙ [clef-flash] 2/3 ETA 0:04
⠹ [clef-flash] 3/3 ETA 0:02
⠸ [clef-flash] 3/3         
                  
2026-10-04 02:07:35 ✔ [clef-flash] 3/3 done in 0:07
res[chosen == TRUE]
   index    question              option         p chosen unsure
   <int>      <char>              <char>     <num> <lgcl> <lgcl>
1:     1      design    randomized_trial 0.9712882   TRUE  FALSE
2:     1 significant                true 0.9463476   TRUE  FALSE
3:     2      design              cohort 0.9566595   TRUE  FALSE
4:     2 significant               false 0.8647017   TRUE  FALSE
5:     3      design diagnostic_accuracy 0.9492614   TRUE  FALSE
6:     3 significant                true 0.8376366   TRUE  FALSE
attr(res, "elapsed")
 trial cohort  model 
 2.184  2.092  2.355 

5.4 Fill a schema

generate() and dmapply() can also fill a closed schema. The result is a validated JSON document in the same shape as an LLM’s structured output. Use this when a decision model takes an LLM’s place in code that expects a document.

A schema is closed when every field is one of:

  • a field with an enum;
  • a boolean;
  • an array whose items is a field with an enum.

is_decidable() says whether a schema is closed, and lists each open field with the reason:

is_decidable(schema(
  "StudySummary",
  field("design", "Study design", enum = c("randomized_trial", "cohort", "case_control")),
  field("n", "Number of participants", type = "integer"),
  field("conclusion", "The authors' main conclusion")
))
[1] FALSE
attr(,"open")
        path                                             reason
1          n an integer with no bounds, or more than 100 values
2 conclusion                                          free text
attr(,"questions")
[1] NA

This schema is closed:

study <- schema(
  "Study",
  field(
    "design",
    "Study design",
    enum = c(
      "randomized_trial", "cohort", "case_control",
      "cross_sectional", "diagnostic_accuracy", "systematic_review"
    )
  ),
  field(
    "significant",
    "Whether the primary result is statistically significant",
    type = "boolean"
  ),
  field(
    "data",
    "Kinds of data analyzed",
    type = "array",
    items = field("data_type", enum = c("imaging", "laboratory", "questionnaire", "genomic"))
  )
)
dm_study <- create_DecisionModel(config_OllamaDecision("clef-flash"), output_schema = study)
msg <- generate(dm_study, abstracts[["model"]])
2026-10-04 02:07:35 [clef-flash] deciding... [generate]
2026-10-04 02:07:40 [clef-flash] done. [generate]
msg
[Decision] {"design":"diagnostic_accuracy","significant":true,"data":["imaging"]}
  design       p = 0.893
  significant  p = 0.883
  data         imaging 0.96, laboratory 0.00, questionnaire 0.00, genomic 0.00 
probabilities(msg)
           path              option           p chosen unsure
         <char>              <char>       <num> <lgcl> <lgcl>
 1:      design diagnostic_accuracy 0.892655230   TRUE  FALSE
 2:      design     cross_sectional 0.042228636  FALSE  FALSE
 3:      design              cohort 0.025346867  FALSE  FALSE
 4:      design        case_control 0.017007728  FALSE  FALSE
 5:      design   systematic_review 0.011680064  FALSE  FALSE
 6:      design    randomized_trial 0.011081477  FALSE  FALSE
 7: significant                true 0.883470758   TRUE  FALSE
 8: significant               false 0.116529242  FALSE  FALSE
 9:        data             imaging 0.960803722   TRUE  FALSE
10:        data          laboratory 0.003694112  FALSE  FALSE
11:        data       questionnaire 0.003503407  FALSE  FALSE
12:        data             genomic 0.003322608  FALSE  FALSE

A batch returns one document per abstract:

dmapply(abstracts, dm_study)

⠼ [clef-flash] 1/3
⠴ [clef-flash] 2/3 ETA 0:09
⠦ [clef-flash] 3/3 ETA 0:05
⠧ [clef-flash] 3/3         
                  
2026-10-04 02:07:54 ✔ [clef-flash] 3/3 done in 0:14
                                                                             trial 
"{\"design\":\"randomized_trial\",\"significant\":true,\"data\":[\"laboratory\"]}" 
                                                                            cohort 
                       "{\"design\":\"cohort\",\"significant\":false,\"data\":[]}" 
                                                                             model 
"{\"design\":\"diagnostic_accuracy\",\"significant\":true,\"data\":[\"imaging\"]}" 
attr(,"validation")
Schema validation: valid 3; invalid 0; unavailable 0; not_validated 0 
attr(,"errors")
[1] index   message
<0 rows> (or 0-length row.names)
attr(,"elapsed")
 trial cohort  model 
 4.712  5.161  4.254 

5.4.1 How a schema becomes questions

The schema is converted into questions before anything is sent:

  • A field with an enum becomes a choice named after the field: “Which value of the field <name> (<description>) does the passage support?”. Each option means only its own label.
  • A boolean field becomes a noul.
  • An array of enum values becomes one noul per value, named <field>[<value>]. The values at or above 0.5 are kept.
  • A field that is not required adds a noul named <field>?, which asks whether the passage gives the field at all. Below 0.5, the field is left out of the document.

as_questions() returns exactly those questions:

as_questions(study)
$design
<Choice>
Which value of the field `design` (Study design) does the passage support?
  randomized_trial
  cohort
  case_control
  cross_sectional
  diagnostic_accuracy
  systematic_review

$significant
<Noul>
Is the field `significant` (Whether the primary result is statistically significant) true, according to the passage?

$`data[imaging]`
<Noul>
Does the field `data` (Kinds of data analyzed) include `imaging`, according to the passage?

$`data[laboratory]`
<Noul>
Does the field `data` (Kinds of data analyzed) include `laboratory`, according to the passage?

$`data[questionnaire]`
<Noul>
Does the field `data` (Kinds of data analyzed) include `questionnaire`, according to the passage?

$`data[genomic]`
<Noul>
Does the field `data` (Kinds of data analyzed) include `genomic`, according to the passage?

An enum value has no meaning beyond its label: case_control is all the model is told about that option. To give the options their meanings, edit the questions and ask them with decide():

qs_study <- as_questions(study)
qs_study[["design"]] <- qs[["design"]]
res_study <- dmapply(abstracts, dm, questions = qs_study)

⠇ [clef-flash] 1/3
⠏ [clef-flash] 2/3 ETA 0:09
⠋ [clef-flash] 3/3 ETA 0:05
⠙ [clef-flash] 3/3         
                  
2026-10-04 02:08:08 ✔ [clef-flash] 3/3 done in 0:14
res_study[chosen == TRUE]
    index            question              option         p chosen unsure
    <int>              <char>              <char>     <num> <lgcl> <lgcl>
 1:     1              design    randomized_trial 0.9323397   TRUE  FALSE
 2:     1         significant                true 0.9455053   TRUE  FALSE
 3:     1       data[imaging]               false 0.9933773   TRUE  FALSE
 4:     1    data[laboratory]                true 0.8904418   TRUE  FALSE
 5:     1 data[questionnaire]               false 0.9736278   TRUE  FALSE
 6:     1       data[genomic]               false 0.9942642   TRUE  FALSE
 7:     2              design              cohort 0.9617279   TRUE  FALSE
 8:     2         significant               false 0.9010801   TRUE  FALSE
 9:     2       data[imaging]               false 0.9929581   TRUE  FALSE
10:     2    data[laboratory]               false 0.9904390   TRUE  FALSE
11:     2 data[questionnaire]               false 0.6998288   TRUE  FALSE
12:     2       data[genomic]               false 0.9949054   TRUE  FALSE
13:     3              design diagnostic_accuracy 0.9328380   TRUE  FALSE
14:     3         significant                true 0.8677437   TRUE  FALSE
15:     3       data[imaging]                true 0.9604992   TRUE  FALSE
16:     3    data[laboratory]               false 0.9957891   TRUE  FALSE
17:     3 data[questionnaire]               false 0.9964573   TRUE  FALSE
18:     3       data[genomic]               false 0.9965528   TRUE  FALSE

5.5 Which to use

Questions (decide()) Schema (generate())
You get probabilities per option a validated JSON document, and the probabilities
Wording yours made from field names and descriptions
Option meanings yes no: an option means its label
Fits judgments, scores, rankings, thresholds code written for an LLM’s structured output

Start with questions. Use a schema when the result must be a document.

5.6 Compare with an LLM

llmapply(), agentapply() and dmapply() all record each call’s wall time as attr(result, "elapsed"). A decision model and an LLM can then be compared on the same passages, each in its best setup.

llm <- create_Ollama("qwen3.5:9b", output_schema = study, temperature = 0)
llm_res <- llmapply(abstracts, llm)
dm_res <- dmapply(abstracts, dm, questions = qs)
summary(attr(llm_res, "elapsed"))
summary(attr(dm_res, "elapsed"))
© 2026 E.D. Gennatas