--- title: "System 1 decisions" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{System 1 decisions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- gptr logo Generative models (System 2) produce text and code. Typed decision models (System 1) answer with a logical value, a choice, or a numeric score, together with their probabilities. gptr returns R vectors that can be used in comparisons, filters and control flow. A batch returns one answer per input. The executed examples use `judge`, a fake classifier with synthetic probabilities. Its setup is hidden to keep the examples short. The outputs show the interface, not model accuracy or calibration. ``` r library(gptr) ``` ## Logical decisions and probabilities ``` r abstracts = c( trial = "We randomized 200 adults to drug A or placebo for 12 weeks.", cohort = "We followed 5000 nurses for 20 years and recorded incident diabetes.", pilot = "A pilot study of 30 patients; the allocation method is not reported." ) is_rct = peter("Is this a randomized trial?", abstracts, model = judge) is_rct #> trial cohort pilot #> TRUE (p=0.94) FALSE (p=0.07) TRUE (p=0.55) #> judge-s1-1.0 . calibration unknown . 2026-10-15 gptr_prob(is_rct) #> trial cohort pilot #> 0.94 0.07 0.55 gptr_prob(is_rct, "probabilities") #> FALSE TRUE #> trial 0.06 0.94 #> cohort 0.93 0.07 #> pilot 0.45 0.55 ``` The `gptr_decision` result is logical. Its value is `TRUE` when P(yes) is at least `threshold` (0.5 by default). `gptr_prob(x)` returns P(yes), while `gptr_prob(x, "confidence")` returns `abs(2 * P(yes) - 1)`; confidence is different from the probability of a positive answer. ## Structured inputs and batch shape A data frame supplies one record per row. An atomic vector or an unnamed list supplies one state per element. A fully named list supplies one structured state; `I(x)` also requests one state for the whole object. Multiple inputs must have compatible batch sizes: a single state can accompany every item in a batch. ``` r studies = data.frame( abstract = unname(abstracts), sample_size = c(200L, 5000L, 30L), row.names = names(abstracts) ) peter("Was allocation randomized?", studies, model = judge) #> trial cohort pilot #> TRUE (p=0.94) FALSE (p=0.07) TRUE (p=0.55) #> judge-s1-1.0 . calibration unknown . 2026-10-15 record = list(abstract = abstracts[["trial"]], sample_size = 200L, allocation = "randomized") length(peter("Was allocation randomized?", record, model = judge)) #> [1] 1 ``` System 1 sends the input states to the selected provider, rather than only an object name. Small structured values can be serialized in full; large or unsupported values are described or truncated. For a large object already loaded in R, derive the small text or structured records the decision needs locally before calling. Check what information the question needs before sending sensitive data; see `?gptr_egress`, `?gptr_security` and [Execution model](execution-model.html) for how inline R computation differs from these inputs. ## Uncertainty in filters, functions and loops `min_confidence` turns answers below the requested confidence into `NA` by default. Unknown confidence is also uncertain when the minimum is positive. For yes/no decisions, `min_confidence = 0.8` requires P(yes) at least 0.9 or at most 0.1. ``` r screened = peter("Is this a randomized trial?", abstracts, model = judge, min_confidence = 0.8) screened #> trial cohort pilot #> TRUE (p=0.94) FALSE (p=0.07) NA (p=0.55) #> judge-s1-1.0 . calibration unknown . 2026-10-15 names(abstracts)[!is.na(screened) & screened] #> [1] "trial" names(abstracts)[is.na(screened)] #> [1] "pilot" ``` Use `isTRUE()` for a scalar decision in `if` or `while` when abstention is possible. Decide separately what to do with an uncertain result; treating `NA` as exclusion is a workflow choice, not a negative model answer. ``` r is_trial = function(text) { peter("Is this a randomized trial?", text, model = judge, min_confidence = 0.8) } included = character() review = character() for (id in names(abstracts)) { decision = is_trial(abstracts[[id]]) if (isTRUE(decision)) included = c(included, id) if (is.na(decision)) review = c(review, id) } list(included = included, review = review) #> $included #> [1] "trial" #> #> $review #> [1] "pilot" ``` The same function can live in an ordinary R file. A call inside a function or loop does not own a recorded document block. A matching System 1 cache entry can avoid another request; that is distinct from document replay. See [Script as history](script-as-history.html). `uncertain = "stop"` signals `gptr_error_s1_uncertain`. `TRUE` or `FALSE` supply a yes/no fallback; a function `function(state, answer)` can return one replacement of the answer's type. Such a function may make another model request if you explicitly implement that escalation, with its own egress, permissions and usage. ## Choices and fractional scores ``` r samples = c(s1 = "Biopsy of the right lower lobe", s2 = "Hepatocytes from a resected liver segment", s3 = "Cortex from an epilepsy resection") tissue = peter("Which tissue is this?", samples, model = judge, choices = c("liver", "lung", "brain", "other")) tissue #> s1 s2 s3 #> lung (p=0.91) liver (p=0.88) brain (p=0.80) #> judge-s1-1.0 . calibration unknown . 2026-10-15 gptr_prob(tissue) #> s1 s2 s3 #> 0.91 0.88 0.80 gptr_prob(tissue, "probabilities") #> liver lung brain other #> s1 0.03 0.91 0.03 0.03 #> s2 0.88 0.04 0.03 0.05 #> s3 0.05 0.05 0.80 0.10 severity = peter("Rate this event's severity.", c(a = "Mild headache", b = "Anaphylaxis requiring adrenaline"), model = judge, levels = c("mild", "moderate", "severe")) severity #> a b #> 0.20 (conf 0.85) 1.88 (conf 0.90) #> judge-s1-1.0 . calibration unknown . 2026-10-15 gptr_prob(severity, "probabilities") #> mild moderate severe #> a 0.85 0.10 0.05 #> b 0.02 0.08 0.90 ``` `gptr_choice` is character; `gptr_prob(tissue)` gives the probability of the selected label. `gptr_score` is numeric, the expected **zero-based** level: here mild = 0, moderate = 1, severe = 2. The example scores are 0.20 and 1.88, not rounded labels. For scores, `gptr_prob(x)` gives confidence; use `"probabilities"` to inspect the distribution over levels. Choice and score confidence are provider-defined measures of concentration, not established probabilities that the answer is correct. ## Provenance and caching ``` r meta = attr(is_rct, "meta") meta[c("model", "engine", "calibrated", "execution", "locality", "cached")] #> $model #> [1] "judge-s1-1.0" #> #> $engine #> [1] "fake" #> #> $calibrated #> [1] NA #> #> $execution #> [1] "native" #> #> $locality #> [1] "local" #> #> $cached #> [1] FALSE FALSE FALSE ``` The metadata also includes usage, request IDs and, when available, a model digest, server version and calibration provenance. `calibrated = NA` means unknown; native decision models do not become calibrated merely by returning probabilities. Emulated answers have `calibrated = FALSE`. Slicing a typed result keeps its probabilities aligned. Matching answers are cached per input and question. With a `.gptr/` workspace the System 1 cache is durable; without one it lives under the session's temporary workspace. Cache identity also depends on the model and answer options. Changed model identity, live mode or a missing entry can require a request. A mutable local model without verified immutable identity cannot promise durable reuse. Use `gptr_cache()` to inspect the cache. ## System 1 routes or judges System 2 ``` r tasks = c("Rename a column.", "Choose a mixed model for repeated measurements.") for (task in tasks) { hard = peter("Does this task need the stronger model?", task, model = judge) session = peter(task, model = if (isTRUE(hard)) strong else fast) print(session$text) } #> [1] "A quick answer from the fast model." #> [1] "A careful answer from the strong model." session = peter("Summarize mtcars in one sentence.", model = summary_model) session |> peter("Did the analysis succeed?", model = judge) #> [1] TRUE (p=0.94) #> judge-s1-1.0 . calibration unknown . 2026-10-15 session$turns #> [1] 1 ``` The piped classifier receives a bounded description of the session's status, designated value and last answer. It adds no agent turn. Judging the answer text is not verification of the calculations or saved artifacts; validate those in R. ## Native Jev, local Ollama and explicit emulation The following examples can contact real providers and are not executed here. Jev reads `TYPESAFE_API_KEY`; `gptr_env()` also recognizes `jev-key`, `JEV_KEY`, `JEV_API_KEY` and `TYPESAFE_KEY` aliases in a key file. ``` r gptr_env("~/keys/jev.env") peter("Is this a randomized trial?", abstracts, model = jev) # Prepare an installed native decision model on your local Ollama server first. peter("Is this a randomized trial?", abstracts, model = "ollama/clef-flash") ``` Native Clef and Clef Flash use Ollama's System One API and require server 0.35.1 or later. Their local-only preflight checks the endpoint and discovered model evidence; an unavailable or unsuitable model fails instead of silently falling back to a cloud model. Ollama chat models use a different adapter and return agent sessions. Emulation uses structured output from a generative model. It must be enabled explicitly in settings; writing an `emulate:` model name alone does not enable it. ``` r gptr_config(system1 = "emulate:anthropic/claude-haiku-4-5", .scope = "session") peter("Is this a randomized trial?", abstracts, model = "emulate:anthropic/claude-haiku-4-5") ``` Emulation is a model request, is marked uncalibrated, and does not create native decision capabilities for a chat model. Select a native model when the workflow requires native probabilities, and assess calibration on the data relevant to that workflow.