<br />

The MediaPipe Decision Maker task evaluates structured decision questions---such as
categorical choices, boolean conditions, and rubric scores---as a deterministic,
calibrated, zero-decoding decision engine, powered by a variety of lightweight
models capable of running on-device, including *EmbeddingGemma 1 and 2* ,
*bidirectional cross-encoders* (Laya, GLiNER2.5-Decide), and *generative Gemma
4*.


<iframe class="mp-demo-frame mp-demo-frame--studio" aria-label="Decision Maker interactive Web SDK demo" loading="lazy" data-src="https://developers.devsite.google/frame/edge/mediapipe/demos/embed/decision_maker.jshtml" allow="camera *; microphone *"></iframe>

[Open in Web Demos](https://google-ai-edge.github.io/mediapipe-samples-web/#/decision/decision_maker)[Edit in StackBlitz](https://stackblitz.com/github/google-ai-edge/mediapipe-samples-web?file=src/tasks/decision-maker.ts)

<br />

Instead of generating free-form text or fragile JSON tokens through
computationally expensive autoregressive decoding, MediaPipe Decision Maker task evaluates
structured decision questions in a *single forward pass* . MediaPipe Decision Maker task
returns calibrated probabilities, winning labels, and continuous expected scores
with minimal runtime overhead. For detailed guidance on choosing between
bi-encoders, cross-encoders, and generative decoders---plus option phrasing,
prior normalization, and `prewarm()` patterns---see
[Prompting and schema design best practices](https://developers.google.com/edge/mediapipe/solutions/decision/decision_maker.md.txt#best-practices).

## Quickstart

Install the SDK for your platform, initialize the task with a model, and run
inference.

### Web

npm`npm install @mediapipe/tasks-decision`[Web demo](https://google-ai-edge.github.io/mediapipe-samples-web/#/decision/decision_maker)·[Edit in StackBlitz](https://stackblitz.com/github/google-ai-edge/mediapipe-samples-web?file=src/tasks/decision-maker.ts)·[Setup guide](https://developers.google.com/edge/mediapipe/solutions/setup_web.md.txt)

```typescript
import { type ChoiceQuestion, DecisionMaker, FilesetResolver } from "@mediapipe/tasks-decision";

// 1. Create the task
const MODEL_URL = "https://huggingface.co/litert-community/embeddinggemma-2-text-270m-litert-lm/resolve/main/embeddinggemma-2-text-270m.litertlm";

const decision = await FilesetResolver.forDecisionTasks(
  "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-decision@latest/wasm"
);
const decisionMaker = await DecisionMaker.createFromOptions(decision, {
  baseOptions: { modelAssetPath: MODEL_URL, delegate: "GPU" },
  maxNumTokens: 512,
});

// 2. Run inference
const categoryQuestion: ChoiceQuestion = {
  criteria: {
    billing: "Refunds, invoices, or subscription charges.",
    technical: "Crashes, login errors, or broken features.",
  },
  normalizePrior: true,
};
const inputContext = "App crashes immediately after logging in.";
const result = await decisionMaker.evaluateChoice(inputContext, categoryQuestion);
```

### Android (Kotlin)

Gradle`implementation("com.google.mediapipe:tasks-decision:latest.release")`[Sample code](https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/decision_maker/android)·[Setup guide](https://developers.google.com/edge/mediapipe/solutions/setup_android.md.txt)

```kotlin
import com.google.mediapipe.tasks.decision.BooleanQuestion
import com.google.mediapipe.tasks.decision.ChoiceQuestion
import com.google.mediapipe.tasks.decision.DecisionMaker
import com.google.mediapipe.tasks.decision.DecisionMakerOptions

// 1. Create the task
val options = DecisionMakerOptions.builder()
    .setModelPath("embeddinggemma-2-text-270m.litertlm")
    .setMaxNumTokens(512)
    .build()
val decisionMaker = DecisionMaker.createFromOptions(context, options)

// 2. Run inference
val intentQuestion = ChoiceQuestion(
    criteria = mapOf(
        "billing" to "Refunds, invoices, or subscription charges.",
        "technical" to "Crashes, login errors, or broken features.",
    ),
    normalizePrior = true,
)
decisionMaker.prewarm(intentQuestion)

val inputContext = "App crashes immediately after logging in."
val choiceResult = decisionMaker.evaluate(inputContext, intentQuestion)

val urgentQuestion = BooleanQuestion(condition = "The user is blocked from using the application.")
val booleanResult = decisionMaker.evaluate(inputContext, urgentQuestion)
```

### Android (Java)

Gradle`implementation 'com.google.mediapipe:tasks-decision:latest.release'`[Sample code](https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/decision_maker/android)·[Setup guide](https://developers.google.com/edge/mediapipe/solutions/setup_android.md.txt)

```java
import com.google.mediapipe.tasks.decision.BooleanQuestion;
import com.google.mediapipe.tasks.decision.BooleanResult;
import com.google.mediapipe.tasks.decision.ChoiceQuestion;
import com.google.mediapipe.tasks.decision.ChoiceResult;
import com.google.mediapipe.tasks.decision.DecisionMaker;
import com.google.mediapipe.tasks.decision.DecisionMakerDelegate;
import com.google.mediapipe.tasks.decision.DecisionMakerOptions;
import com.google.mediapipe.tasks.decision.ScoringMode;
import java.util.Map;

// 1. Create the task
DecisionMakerOptions options = new DecisionMakerOptions(
    "embeddinggemma-2-text-270m.litertlm",
    null,
    null,
    512,
    DecisionMakerDelegate.CPU
);
DecisionMaker decisionMaker = DecisionMaker.createFromOptions(context, options);

// 2. Run inference
ChoiceQuestion intentQuestion = new ChoiceQuestion(
    Map.of(
        "billing", "Refunds, invoices, or subscription charges.",
        "technical", "Crashes, login errors, or broken features."
    ),
    0.0f,
    "",
    ScoringMode.LENGTH_NORMALIZED_LOG_PROB,
    true
);
decisionMaker.prewarm(intentQuestion);

String inputContext = "App crashes immediately after logging in.";
ChoiceResult choiceResult = decisionMaker.evaluate(inputContext, intentQuestion);

BooleanQuestion urgentQuestion = new BooleanQuestion(
    "The user is blocked from using the application.",
    0.5f,
    0.0f,
    false
);
BooleanResult booleanResult = decisionMaker.evaluate(inputContext, urgentQuestion);
```

### iOS

SwiftPM`https://github.com/google-ai-edge/mediapipe → MediaPipeTasksDecision`[Package.swift](https://developers.google.com/edge/mediapipe/solutions/setup_ios.md.txt#package_swift)·[Sample code](https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/decision_maker/ios)·[Setup guide](https://developers.google.com/edge/mediapipe/solutions/setup_ios.md.txt)

```swift
import Foundation
import MediaPipeTasksDecision

// 1. Create the task
guard let modelPath = Bundle.main.path(
    forResource: "embeddinggemma-2-text-270m",
    ofType: "litertlm"
) else { return }

let options = DecisionMakerOptions(modelPath: modelPath, maxNumTokens: 512, delegate: .cpu)
let decisionMaker = try DecisionMaker(options: options)

// 2. Run inference
let intentQuestion = ChoiceQuestion(
    criteria: [
        "billing": "Refunds, invoices, or subscription charges.",
        "technical": "Crashes, login errors, or broken features.",
    ],
    normalizePrior: true
)
try decisionMaker.prewarm(question: intentQuestion)

let inputText = "App crashes immediately after logging in."
let choiceResult = try decisionMaker.evaluate(text: inputText, question: intentQuestion)
```

### Python

pip`pip install mediapipe`[Sample code](https://colab.research.google.com/github/google-ai-edge/mediapipe-samples/blob/main/examples/decision_maker/python/decision_maker.ipynb)·[Setup guide](https://developers.google.com/edge/mediapipe/solutions/setup_python.md.txt)

```python
import mediapipe as mp

BaseOptions = mp.tasks.BaseOptions
ChoiceQuestion = mp.tasks.decision.ChoiceQuestion
DecisionMaker = mp.tasks.decision.DecisionMaker
DecisionMakerOptions = mp.tasks.decision.DecisionMakerOptions

# 1. Create the task
options = DecisionMakerOptions(
    base_options=BaseOptions(model_asset_path="embeddinggemma-2-text-270m.litertlm"),
    max_num_tokens=512,
)

with DecisionMaker.create_from_options(options) as maker:
    # 2. Run inference
    intent_q = ChoiceQuestion(
        criteria={
            "billing": "Refunds, invoices, or subscription charges.",
            "technical": "Crashes, login errors, or broken features.",
        },
        normalize_prior=True,
    )
    maker.prewarm(intent_q)

    context_text = "App crashes immediately after logging in."
    res = maker.evaluate_choice(context_text, intent_q)
    batch_res = maker.evaluate_batch(
        contexts=["Double charge error", "App crashes on start"],
        questions={"intent": intent_q},
    )
```

### Flutter

pub`flutter pub add mediapipe_decision`[Live gallery](https://google.github.io/flutter-mediapipe/)·[Sample code](https://github.com/google/flutter-mediapipe/tree/main/samples/decision/decision_maker)·[Setup guide](https://developers.google.com/edge/mediapipe/solutions/setup_flutter.md.txt)

```dart
import 'package:mediapipe_decision/mediapipe_decision.dart';

Future<void> decide() async {
  final capabilities = await queryDecisionMakerCapabilities();
  final task = await DecisionMaker.create(
    DecisionMakerOptions(
      model: DecisionModels.layaS256,
      // The CPU natively, the GPU in browsers.
      delegate: capabilities.supportedDelegates.first,
    ),
  );
  try {
    const text = 'My order arrived broken and I want my money back.';
    final refund = await task.evaluateBoolean(
      text,
      BooleanQuestion('The customer wants a refund.'),
    );
    final topic = await task.evaluateChoice(
      text,
      ChoiceQuestion({
        'shipping': 'A problem with delivery or a damaged package',
        'billing': 'A question about a charge or a refund',
        'other': 'Anything else',
      }),
    );
    final mood = await task.evaluateScore(
      text,
      ScoreQuestion(['very unhappy', 'unhappy', 'neutral', 'happy']),
    );
    print('${refund.value} ${topic.selectedKey} ${mood.expectedScore}');
  } finally {
    await task.dispose();
  }
}
```

## Task details

This section describes the capabilities, inputs, outputs, and configuration
options of this task.

### Features

- **Zero-decoding decision inference**: Eliminates token generation latency by scoring structured decision primitives directly from model representations.
- **Multimodal support** : Accepts UTF-8 text across all backends, plus RGB images (`MPImage`, JPEG/PNG bytes) with EmbeddingGemma 2 (440M and 740M) and 16 kHz audio samples with EmbeddingGemma 2 Text / Vision / Audio (740M) in both the input context and decision options.
- **First-class JSON integration** : Built-in JSON parsing utilities evaluate TypeSafe Jev JSON payloads and OpenAI `json_schema` specifications without custom adapter code.
- **Cross-platform consistency**: Uniform API architecture available across Android (Kotlin), iOS (Swift), Python, and Web (TypeScript/JavaScript).

### Input and output formats

- **Input Context**: Multimodal state containing text, image (440M and 740M EmbeddingGemma 2), or 16 kHz audio clips (740M EmbeddingGemma 2).
- **Question Primitives** :
  - `ChoiceQuestion`: Multi-class categorical decision among K options.
  - `BooleanQuestion`: Binary condition validation (True / False or Yes / No).
  - `ScoreQuestion`: Ordinal rubric grading with automated expected score calculation.
- **Outputs**: Winning key, option probabilities, boolean value, and expected continuous score.

### Supported model backends

| Model Backend | Architecture | Typical Latency | Key Strengths |
|---|---|---|---|
| **EmbeddingGemma 1 \& 2** | Bi-Encoder (270M / 300M / 440M / 740M) | Ultra-low latency | High throughput, multimodal text/vision/audio scoring (440M \& 740M), multi-span LogSumExp document pooling, 0 MB extra RAM using `UniversalEmbedder`. |
| **Gemma 4 E2B \& E4B** | Autoregressive Decoder (2B / 4B) | Single Prefill Pass | Complex compositional logic, counterfactual policy exceptions. |
| **Laya \& GLiNER2.5-Decide** | Cross-Encoder (149M / 166M) | Single Cross-Attention Pass | Joint context-label attention. |

> [!NOTE]
> **Note:** On Web (`@mediapipe/tasks-decision`), `DecisionMaker` defaults to the `GPU` (WebGPU) delegate. `.litertlm` containers with external weights (*EmbeddingGemma 2* and *Gemma 4* ) require WebGPU on Web (`delegate: 'GPU'` or omitted), whereas `delegate: 'CPU'` on Web is only supported for `.task` / inline `.tflite` models such as *Laya*.

### Execution lifecycle

The Decision Maker operates on a three-step execution lifecycle designed to
optimize latency and memory:

1. *Create (`DecisionMaker`)* : Initialize the decision engine from a model path, file descriptor, or buffer. Existing `UniversalEmbedder` providers can also be shared to achieve 0 MB extra RAM consumption.
2. *Prewarm (`prewarm` / `Prewarm` / `prewarmJson`)*: Executed once during setup. For embedding backends, it computes candidate option embeddings, performs within-question centroid whitening, and L2-normalizes vector representations. For logit backends, it pre-populates KV cache prefixes.
3. *Evaluate (`evaluate` / `Evaluate` / `evaluateJson`)*: Executed per request. Scores context queries against cached decision structures in a single pass, returning structured results with ultra-low latency per question on CPU.

### Configuration options

This task has the following configuration options:

### Web

| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
| `baseOptions.modelAssetPath` | Source of the `.litertlm`, `.task`, or `.tflite` model asset (`baseOptions.modelAssetPath` / `modelAssetBuffer` on Web, `modelPath` / `modelAssetFileDescriptor` / `modelAssetBuffer` on Android, `modelSource` / `modelPath` / `modelData` on iOS, or `base_options.model_asset_path` / `model_asset_buffer` in Python). | Path / Buffer | Required |
| `maxNumTokens` | Maximum number of tokens allowed for input context processing. | `Integer` | `4096` |
| `baseOptions.delegate` | Hardware acceleration delegate (`CPU` or `GPU`). On Web, `.litertlm` containers with external weights (*EmbeddingGemma 2* , *Gemma 4* ) require `'GPU'` (WebGPU); `'CPU'` on Web is supported for `.task` or inline `.tflite` models (such as *Laya*). | {`CPU`, `GPU`} | `GPU` |

### Android

| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
| `modelPath` | Source of the `.litertlm`, `.task`, or `.tflite` model asset (`modelPath`, `modelAssetFileDescriptor`, or `modelAssetBuffer` on `DecisionMakerOptions`). | Path / FD / Buffer | Required |
| `maxNumTokens` | Maximum number of tokens allowed for input context processing. | `Integer` | `4096` |
| `delegate` | Hardware acceleration delegate (`DecisionMakerDelegate.CPU` or `GPU`). | {`CPU`, `GPU`} | `CPU` |

### iOS

| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
| `modelPath` | Source of the `.litertlm`, `.task`, or `.tflite` model asset (`modelSource`, `modelPath`, or `modelData` on `DecisionMakerOptions`). | Path / Data | Required |
| `maxNumTokens` | Maximum number of tokens allowed for input context processing. | `Integer` | `4096` |
| `delegate` | Hardware acceleration delegate (`.cpu` or `.gpu`). | {`CPU`, `GPU`} | `CPU` |

### Python

| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
| `base_options.model_asset_path` | Source of the `.litertlm`, `.task`, or `.tflite` model asset (`base_options.model_asset_path` or `model_asset_buffer`). | Path / Buffer | Required |
| `max_num_tokens` | Maximum number of tokens allowed for input context processing. | `Integer` | `4096` |
| `base_options.delegate` | Hardware acceleration delegate (`BaseOptions.Delegate.CPU` or `GPU`). | {`CPU`, `GPU`} | `CPU` |

### Flutter

| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
| `model / modelPath / modelBytes` | Source of the model asset: a pinned `DecisionModels` model, the path to a `.litertlm` or `.task` file (a URL in browsers), or the model's bytes. Supply exactly one. | `DownloadAsset` / `String` / `Uint8List` | Required |
| `maxNumTokens` | Maximum number of tokens allowed for input context processing. `0` keeps the model's own token budget (512 for EmbeddingGemma 2). | Integer \>= 0 | `0` |
| `delegate` | Hardware acceleration delegate. Native platforms run on `Delegate.cpu`; browsers run on `Delegate.gpu` (WebGPU) only. `queryDecisionMakerCapabilities()` reports the delegates available on the current platform. | {`Delegate.cpu`, `Delegate.gpu`} | `Delegate.cpu` |

### Task output

Depending on the evaluation method called (`evaluate` on Android/iOS, `evaluateBoolean` / `evaluateChoice` / `evaluateScore` on Web and in Dart, or `evaluate_boolean` / `evaluate_choice` / `evaluate_score` in Python), `DecisionMaker` returns a structured `BooleanResult`, `ChoiceResult`, or `ScoreResult`:

| Field | Result Type | Type / Range | Description |
|---|---|---|---|
| `value` | `BooleanResult` | `boolean` | `true` when `probabilityTrue` meets or exceeds the question `threshold` (default `0.5`). |
| `probabilityTrue` | `BooleanResult` | `float` (`[0.0, 1.0]`) | Calibrated probability that the condition holds (`probability_true` in Python). |
| `selectedKey` | `ChoiceResult`, `ScoreResult` | `string` | Key of the winning option or highest-probability rubric level (`selected_key` in Python; in Dart, a `ScoreResult`'s key is the level's index as text). |
| `probabilities` | `ChoiceResult`, `ScoreResult` | `Record<string, float>` / `float[]` (`[0.0, 1.0]`) | Normalized probability map across K options (`ChoiceResult`) or ordered array across K rubric levels (`ScoreResult`). In Dart, a `Map<String, double>` in the question's order for `ChoiceResult` and a `List<double>`, lowest level first, for `ScoreResult`. |
| `predictionSet` | `ChoiceResult` | `string[]` | 90% conformal prediction set of candidate option keys (`prediction_set` in Python). |
| `expectedScore` | `ScoreResult` | `float` (`[0.0, K - 1]`) | Continuous probability-weighted expected score across the ordered rubric levels (`expected_score` in Python). |
| `confidence` | All results | `float` (`[0.0, 1.0]`) | Normalized certainty score derived from probability margin and entropy. |

#### Example `DecisionMaker` result payloads

The following examples show the return values for `BooleanQuestion`, `ChoiceQuestion`, and `ScoreQuestion`:

```javascript
// BooleanResult (from evaluate / evaluateBoolean)
{ value: true, probabilityTrue: 0.9420, confidence: 0.8840 }

// ChoiceResult (from evaluate / evaluateChoice)
{
  selectedKey: "lights_control",
  probabilities: { lights_control: 0.9120, thermostat: 0.0510, security_check: 0.0370 },
  predictionSet: ["lights_control"],
  confidence: 0.8610
}

// ScoreResult (from evaluate / evaluateScore)
{
  selectedKey: "Routine preference adjustment",
  expectedScore: 0.21,
  probabilities: [0.8200, 0.1500, 0.0300],
  confidence: 0.7900
}
```

## Models

![Decision Maker pipeline overview](https://developers.google.com/static/edge/mediapipe/images/solutions/diagrams/decision_maker.svg)

| Model name | Input | Quantization type | Model Card | Versions |
|---|---|---|---|---|
| [EmbeddingGemma 2 Text (270M)](https://huggingface.co/litert-community/embeddinggemma-2-text-270m-litert-lm/resolve/main/embeddinggemma-2-text-270m.litertlm) | Text context and options (`string`, `Dict[string, string]`, or `List[string]`) | int4 | [Model card](https://huggingface.co/litert-community/embeddinggemma-2-text-270m-litert-lm) | [Latest](https://huggingface.co/litert-community/embeddinggemma-2-text-270m-litert-lm/resolve/main/embeddinggemma-2-text-270m.litertlm) |
| [EmbeddingGemma 2 Text / Vision (440M)](https://huggingface.co/litert-community/embeddinggemma-2-text-vision-440m-litert-lm/resolve/main/embeddinggemma-2-text-vision-440m.litertlm) | Text and image context and options (`string`, `Dict[string, string]`, `List[string]`, `MPImage`, or JPEG/PNG bytes) | Mixed Precision (int4+int8) | [Model card](https://huggingface.co/litert-community/embeddinggemma-2-text-vision-440m-litert-lm) | [Latest](https://huggingface.co/litert-community/embeddinggemma-2-text-vision-440m-litert-lm/resolve/main/embeddinggemma-2-text-vision-440m.litertlm) |
| [EmbeddingGemma 2 Text / Vision / Audio (740M)](https://huggingface.co/litert-community/embeddinggemma-2-740m-litert-lm/resolve/main/embeddinggemma-2-740m.litertlm) | Text, image, and 16 kHz audio context and options (`string`, `Dict[string, string]`, `List[string]`, `MPImage` / JPEG/PNG bytes, or 16 kHz audio samples) | Mixed Precision (int2/int4/int8) | [Model card](https://huggingface.co/litert-community/embeddinggemma-2-740m-litert-lm) | [Latest](https://huggingface.co/litert-community/embeddinggemma-2-740m-litert-lm/resolve/main/embeddinggemma-2-740m.litertlm) |
| [EmbeddingGemma 300M](https://storage.googleapis.com/mediapipe-models/text_embedder/embedding_gemma/int4int8/latest/embedding_gemma.task) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | Mixed Precision (int4+int8) | [Model card](https://huggingface.co/litert-community/embeddinggemma-300m) | [Latest](https://storage.googleapis.com/mediapipe-models/text_embedder/embedding_gemma/int4int8/latest/embedding_gemma.task) |
| [Gemma 4 E2B](https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | int4 | [Model card](https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm) | [Latest](https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm) |
| [Gemma 4 E4B](https://huggingface.co/litert-community/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it.litertlm) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | int4 | [Model card](https://huggingface.co/litert-community/gemma-4-E4B-it-litert-lm) | [Latest](https://huggingface.co/litert-community/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it.litertlm) |
| [Laya (256-token)](https://storage.googleapis.com/mediapipe-models/decision_maker/laya/float32/laya_s256/latest/laya_s256.task) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | float32 | [Model card](https://huggingface.co/litert-community/laya-LiteRT) | [Latest](https://storage.googleapis.com/mediapipe-models/decision_maker/laya/float32/laya_s256/latest/laya_s256.task) |
| [Laya (512-token)](https://storage.googleapis.com/mediapipe-models/decision_maker/laya/float32/laya_s512/latest/laya_s512.task) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | float32 | [Model card](https://huggingface.co/litert-community/laya-LiteRT) | [Latest](https://storage.googleapis.com/mediapipe-models/decision_maker/laya/float32/laya_s512/latest/laya_s512.task) |
| [GLiNER2.5-Decide (256-token)](https://storage.googleapis.com/mediapipe-models/decision_maker/gliner/float16/gliner_s256/latest/gliner_s256.task) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | float16 | [Model card](https://huggingface.co/litert-community/GLiNER2.5-Decide-LiteRT) | [Latest](https://storage.googleapis.com/mediapipe-models/decision_maker/gliner/float16/gliner_s256/latest/gliner_s256.task) |
| [GLiNER2.5-Decide (512-token)](https://storage.googleapis.com/mediapipe-models/decision_maker/gliner/float16/gliner_s512/latest/gliner_s512.task) | Text context (`string`, `Dict[string, string]`, or `List[string]`) | float16 | [Model card](https://huggingface.co/litert-community/GLiNER2.5-Decide-LiteRT) | [Latest](https://storage.googleapis.com/mediapipe-models/decision_maker/gliner/float16/gliner_s512/latest/gliner_s512.task) |

## Decision primitives

All decision tasks in the MediaPipe Decision Maker task are composed of three fundamental
decision question primitives (see [Task output](https://developers.google.com/edge/mediapipe/solutions/decision/decision_maker.md.txt#task_output) for result field
schemas):

| Primitive | Question Type | Result Type | Description |
|---|---|---|---|
| *Boolean* | `BooleanQuestion` | `BooleanResult` | Evaluates whether a natural-language condition holds against the context using a configurable `threshold` (default `0.5`). |
| *Choice* | `ChoiceQuestion` | `ChoiceResult` | Selects 1 of K mutually exclusive candidate options defined by text descriptions, images, or audio samples. |
| *Score* | `ScoreQuestion` | `ScoreResult` | Evaluates an ordered K-level rubric from lowest (`0`) to highest (`K - 1`) grade and computes the continuous probability-weighted expected score. |

## Migration and comparison

This section details how MediaPipe Decision Maker task compares to cloud alternatives, such
as OpenAI Structured Outputs and server-side Jev APIs.

### Architectural comparison

| Dimension | MediaPipe Decision Maker | TypeSafe Jev API | OpenAI Structured Outputs API |
|---|---|---|---|
| *Execution Target* | *On-Device Edge* (Android, iOS, WebGPU/Wasm, Desktop, Python) | *Server / Cloud / Python Harness* (Vertex AI, Gemini API) | *Cloud HTTP API* (`/v1/chat/completions`, `json_schema`) |
| *Underlying Engine* | *Multi-Backend Engine* : 1. Bi-Encoder (EmbeddingGemma 1 \& 2) 2. Cross-Encoder (Laya, GLiNER2.5) 3. Single-pass Logits (Gemma 4) | *Prompted Generative Logits* (`Jev-Gemma`) or *Bi-Encoder Dot-Product* (`Jev-Embed`) | *Autoregressive Constrained Decoding* (FSM token masking over JSON Schema) |
| *Core Primitives* | `ChoiceQuestion`, `BooleanQuestion`, `ScoreQuestion` | `choice`, `noul` (boolean), `score` | Arbitrary JSON Schema (`enum`, `boolean`, `number`), no built-in expected value calculation |
| *Option Caching (`Prewarm`)* | *Explicit `Prewarm()` API* : Embeds and whitens all K options once up front; runtime executes *one query forward pass* (`O(1)` in K). | Re-evaluates prompt or option embeddings per request unless manually cached. | Server-side automatic prefix caching only (still executes token decoding). |
| *Multimodal Support* | *Built-in for Context AND Options* (EmbeddingGemma 2 440M/740M): Accepts text, `MPImage`/JPEG/PNG, and 16 kHz audio in inputs and candidates. | Text / JSON state only (`state: dict | str`). | Multimodal in input prompt, *text-only* in output schema options. |
| *Latency and Memory* | *Ultra-low latency* per question on CPU, *157 MB* bundle (*0 MB extra RAM* sharing `UniversalEmbedder`). | Network RTT or uncached Python GPU inference. | Network RTT, billed per input and output token. |

### Built-in JSON support

Both TypeSafe Jev JSON payloads and OpenAI `json_schema` definitions are parsed
directly by `DecisionMaker` (`evaluateJson` and `prewarmJson`) with **zero
custom adapter code required**.

#### 1. Built-in Jev JSON payload format (`evaluateJson`)

Evaluate unmodified Jev payloads (`"state"` + `"questions"`):

```json
{
  "state": "Cancel my flight and refund my credit card immediately.",
  "questions": {
    "intent": {
      "type": "choice",
      "instructions": "Determine the user's primary travel intent.",
      "normalize_prior": true,
      "criteria": {
        "book_flight": "Search or book a new flight itinerary.",
        "cancel_refund": "Cancel an existing reservation and request a refund.",
        "baggage_claim": "Report lost or delayed luggage."
      }
    },
    "is_financial": {
      "type": "noul",
      "condition": "The request involves a payment, charge, or refund.",
      "threshold": 0.5
    }
  }
}
```

#### 2. Built-in OpenAI `json_schema` property mapping

`DecisionMaker` automatically maps standard OpenAI `json_schema` property
objects onto the built-in Decision Maker decision primitives:

| OpenAI JSON Schema property | Decision Maker primitive mapping | Decision Maker behavior |
|---|---|---|
| `{"type": "string", "enum": [...], "description": "..."}` | `ChoiceQuestion(criteria={v: "" for v in enum}, instructions=description)` | Evaluates calibrated probabilities for **every** enum value in 1 non-generative pass. |
| `{"type": "boolean", "description": "..."}` | `BooleanQuestion(condition=description)` | Calibrated probability of True with customizable decision threshold. |
| `{"type": "integer", "minimum": 1, "maximum": 5, "description": "..."}` | `ScoreQuestion(rubric=["1".."5"], instructions=description)` | Calculates continuous expected score rather than quantized integer. |

## Best practices

This section outlines best practices for designing schemas, writing prompts, and
configuring parameters in **MediaPipe Decision Maker task** across its three model
architectures:

1. **Bi-Encoders** (*EmbeddingGemma 1 \& 2*)
2. **Autoregressive Generative Models** (*Gemma 4 E2B \& E4B*)
3. **Bidirectional Cross-Encoders** (*Laya, GLiNER2.5-Decide*)

Because scoring mechanics vary across these model families, following these
guidelines ensures maximum accuracy, lower latency, and calibrated predictions.

### Do's and don'ts

| Design Dimension | Do (Recommended) | Don't (Avoid) | Why It Matters |
|---|---|---|---|
| **Option Descriptions** (*Choice* \& *Score*) | Provide rich, self-contained semantic descriptions for every key (e.g., `"billing_refund": "Disputed charges, refund requests, or invoice errors"`). | Avoid passing bare opaque keys (`"0"`, `"1"`, `"cat_a"`) with empty descriptions or repeated boilerplate. | Bi-encoders project options into metric space; distinct semantic phrases maximize separation between options. |
| **Boolean Conditions** (*Boolean*) | Phrase conditions as single, affirmative, testable propositions (e.g., `"The customer is requesting a refund or billing credit"`). | Avoid using double negations (`"The request is NOT ineligible"`) or compound `"A unless B"` exceptions inside a single string. | Affirmative propositions align cleanly with internal hypothesis templates across all model backends. |
| **Prior Normalization** (`normalize_prior`) | Enable `normalize_prior = True` when option descriptions vary significantly in length, specificity, or frequency. | Avoid leaving `normalize_prior = False` when one catch-all option is much broader or more generic than others. | Subtracts the null-context baseline score (0 ms cost when prewarmed) so generic labels don't dominate borderline inputs. |
| **Temperature Calibration** (`temperature`) | Leave `temperature = 0.0` (default) to use auto-calibrated temperatures (approximately 0.045 for EmbeddingGemma, 1.0 for Gemma 4 / Laya). | Avoid hardcoding `temperature = 1.0` on EmbeddingGemma unless unscaled cosine probabilities are specifically required. | Raw cosine similarities live in a narrow -0.15 to +0.35 band. Default `T = 1.0` yields near-uniform probabilities (ECE \> 0.30), while auto-calibrated `T = 0.045` achieves ECE = 0.048. |
| **3-Way Verification \& Abstention** | Use standard keys (`"supported"`, `"contradicted"`, `"insufficient"`) with distinct evidence descriptions. | Avoid mixing multiple overlapping "unsure" / "maybe" options in the same question without clear definitions. | The compiler recognizes 3-way verification schemas, handles neutral centroid subtraction, and prevents decision bias. |
| **Static vs. Dynamic Schemas** | Call `prewarm()` once at startup for static or repeatedly evaluated schemas. | Avoid re-compiling or omitting `prewarm()` on hot loops with large option sets (10 or more options). | `prewarm()` caches option embeddings (EmbeddingGemma) and KV prefixes (Gemma 4), reducing runtime evaluation to `O(1)` in the number of options. |

### Schema design rules

Follow these design rules to get the most out of any MediaPipe Decision Maker task model.

#### 1. Write self-contained option descriptions

For `ChoiceQuestion` and `ScoreQuestion`, always pair concise keys with clear,
descriptive strings.

```python
import mediapipe as mp

BooleanQuestion = mp.tasks.decision.BooleanQuestion
ChoiceQuestion = mp.tasks.decision.ChoiceQuestion

# GOOD: Rich, distinct semantic descriptions
support_routing = ChoiceQuestion(
    criteria={
        "billing_refund": "Disputed charges, refund requests, invoice errors, or payment failures.",
        "technical_bug": "App crash, UI freeze, login failure, sync error, or broken functionality.",
        "feature_request": "Suggestions for new capabilities, UI enhancements, or integrations.",
    }
)

# AVOID: Bare keys with empty or repetitive descriptions
bad_routing = ChoiceQuestion(
    criteria={
        "option_1": "Option 1",
        "option_2": "Option 2",
    }
)
```

#### 2. Phrase boolean conditions affirmatively

Avoid double negatives or tricky condition phrasing. Keep boolean conditions
affirmative and concise.

```python
# GOOD: Direct, affirmative condition
is_vip = BooleanQuestion(condition="The customer holds an active Premium or Enterprise tier subscription.")

# AVOID: Double negatives
is_not_unsubscribed = BooleanQuestion(condition="The user does NOT want to opt out of notifications.")
```

#### 3. Use prior normalization for unequal options

When one option is naturally broader than others (e.g., an `"other_general"`
catch-all category), enable prior normalization. This evaluates the schema
without context at prewarm time and subtracts the baseline preference at zero
runtime cost.

```python
normalized_choice = ChoiceQuestion(
    criteria={
        "password_reset": "Specific requests to reset account password or unlock 2FA.",
        "general_inquiry": "General questions about company policies, hours, or contact info.",
    },
    normalize_prior=True,
)
```

### Model optimization

Follow the guide for your chosen model family.

#### EmbeddingGemma 1 \& 2 (Bi-Encoders)

- **Models**: EmbeddingGemma 2 (270M Text, 440M Text / Vision, 740M Text / Vision / Audio), EmbeddingGemma 1 (300M)
- **Performance**: Ultra-low latency on CPU and GPU
- **Architecture**: Symmetric bi-encoder embedding context and candidate options into a shared 768D L2-normalized vector space.

1. **Provide Concrete Exemplars** : Include specific synonyms and keywords in option descriptions (e.g., `"technical_bug": "App crash, freeze, login
   failure, sync error, or broken UI"`).
2. **Focus Instructions on the Decision Axis** : Keep optional instructions concise (1 sentence) describing *what dimension* to classify (e.g., `"Classify the primary intent of the support ticket."`).
3. **Automatic Long-Context Handling** : For long documents (\> 256 tokens, up to 2,048 tokens), `DecisionMaker` automatically chunks context into overlapping 512-token spans using head-anchored LogSumExp pooling. Placing key metadata near the top of the context takes advantage of the head-span anchor weight (0.35).
4. **Use Rubric Descriptions with ScoreQuestion**: For ordinal ratings (1--5), populate descriptions for each level. Decision Maker computes the probability-weighted expected score across levels, yielding smooth continuous scores with higher correlation than discrete argmax picking.

#### Gemma 4 (E2B \& E4B)

- **Models**: Gemma 4 E2B (2B) \& E4B (4B)
- **Performance** : Evaluates decisions in a **single prefill forward pass** with zero autoregressive token generation.

1. **Leverage for Complex Policy Rules**: Ideal for tasks involving counterfactual reasoning, multi-rule exceptions, or complex conditional logic where bi-encoders struggle.
2. **Put Explicit Rules in Instructions** : Place explicit constraints directly in the `instructions` field (e.g., `"Approve refund ONLY IF purchased within
   14 days; EXCEPT digital downloads which are non-refundable unless
   unplayed."`).
3. **Prewarm for Prefix Caching** : Always call `prewarm()` so `DecisionMaker` caches the KV states of the prompt prefix.

#### Laya \& GLiNER2.5-Decide (Cross-Encoders)

- **Models**: Laya (149M), GLiNER2.5-Decide (166M)
- **Architecture**: Bidirectional DeBERTa/ModernBERT cross-encoders that jointly attend to the input context and candidate options within a 256- or 512-token window.

1. **Keep Option Sets Compact When Possible** : When all candidate options and the input context fit within the model's 256- or 512-token window (typically 8 or fewer options), `DecisionMaker` packs all options alongside the context into a single encoder forward pass (\~4× faster than pairwise evaluation). For larger option sets or very long option descriptions, `DecisionMaker` automatically falls back to evaluating options across multiple passes.
2. **Keep Option Descriptions Concise**: Use high-signal 5--15 word descriptions to save token budget for the context.

### Common pitfalls

Avoid these common problems and apply the recommended fixes.

#### Pitfall 1: Unsplit counterfactual or "unless" exceptions on bi-encoders

**Problem**: Bi-encoders compute independent embeddings for context and options.
When a single question contains base rules, "unless" exceptions, and
counterfactual overrides, lexical overlap can pull the bi-encoder toward the
wrong label.

- **Solution A --- Decompose into atomic `BooleanQuestion`s (Recommended)** :
  Because Decision Maker embeds the input context **only once** and reuses the
  query vector across all prewarmed questions, evaluating 3 atomic boolean
  questions adds minimal additional latency over evaluating a single question.

  ```python
  # AVOID: Single compound question with "unless" / "except" clauses on Bi-Encoders
  bad_policy = BooleanQuestion(
      condition=(
          "The order is eligible for a refund (purchased within 30 days, UNLESS it is "
          "a final-sale clearance item, EXCEPT when the item arrived physically damaged)."
      )
  )

  # RECOMMENDED on Bi-Encoders (EG1 / EG2): Decompose into atomic BooleanQuestions.
  # Decision Maker embeds the input context ONCE and evaluates all 3 conditions.
  # `maker` is a DecisionMaker instance created as shown in the Quickstart.
  atomic_policy_schema = {
      "within_30_days": BooleanQuestion(condition="The item was purchased within the last 30 days."),
      "is_clearance": BooleanQuestion(condition="The item is a final-sale or clearance item."),
      "arrived_damaged": BooleanQuestion(
          condition="The item arrived physically damaged, broken, or defective."
      ),
  }

  order_text = "Purchased 10 days ago on final-sale clearance, arrived cracked."
  res = maker.evaluate_batch(contexts=[order_text], questions=atomic_policy_schema)[0]

  # Combine booleans deterministically in application logic
  is_eligible = res["arrived_damaged"].value or (
      res["within_30_days"].value and not res["is_clearance"].value
  )
  ```
- **Solution B --- Route to Gemma 4 or Cross-Encoder models** : If your
  application accepts arbitrary, user-defined policy prompts that cannot be
  decomposed ahead of time, route evaluations to **Gemma 4** or **Laya**,
  which perform full cross-attention across rule exceptions and context.

#### Pitfall 2: Double negations and negated option keys

**Problem** : Negated conditions (`"is NOT ineligible"`) or keys sharing root
words (`"not_spam"` versus `"spam"`) create semantic ambiguity.

- **Avoid** :
  `python
  BooleanQuestion(condition="The user does NOT want to opt out of notifications.")
  ChoiceQuestion(criteria={"not_spam": "Legitimate email", "spam": "Unsolicited email"})`

- **Do instead** :
  `python
  BooleanQuestion(condition="The user wants to receive notifications.")
  ChoiceQuestion(
  criteria={
  "legitimate_inbox": "Personal, work, or transactional message expected by user.",
  "unsolicited_spam": "Promotional bulk mail, phishing, or unwanted advertisement.",
  }
  )`

#### Pitfall 3: Bare numeric or code keys without descriptions

**Problem** : Opaque numeric keys like `"0"`, `"1"`, `"2"` with empty
descriptions provide no semantic vector target for bi-encoders.

- **Avoid** : `python
  ChoiceQuestion(criteria={"0": "", "1": "", "2": ""})`
- **Do instead** : `python
  ChoiceQuestion(
  criteria={
  "0": "No defect visible upon inspection.",
  "1": "Minor cosmetic scratch or superficial blemish.",
  "2": "Severe structural crack or functional failure.",
  }
  )`

> [!NOTE]
> **Note:** When 3 or more numeric keys are used with descriptions, Decision Maker automatically formats them as `"title: Level 0 | text: ..."` to prevent confusion with binary boolean values.