Python API

Decision

class RPA.Decision.Decision(backend: str | DecisionBackend | Callable | None = None, threshold: float = 0.8, audit_log: str | None = None, api_key: str | None = None, model: str | None = None, base_url: str | None = None, **backend_options: Any)

Library for making small, typed decisions with a decision model (also called a System One model) such as Jev or the open-weight Laya.

Unlike a generative LLM, a decision model does not write text. You give it a state (the thing to look at) and a question with a closed answer space, and it returns one of the allowed answers together with a calibrated probability. That makes it a natural fit for the branch points of an automation:

  • routing work items (invoice / credit note / reminder / junk)

  • classifying failures (business exception vs. application exception)

  • guardrails before a side effect (“is it safe to auto-reply to this?”)

  • deciding when to hand off to a human, using the confidence as the gate

Three kinds of question are supported, mirroring the model APIs:

Kind

Keyword

Returns

choice

Decide, Decide Or Escalate

one option + probability for every option

yes/no

Check

probability that the proposition is true

score

Score

position on an ordered rubric

Every call is appended to an in-memory audit log (Get Decision Audit Log) and, optionally, to a JSON Lines file. Store those records in your work item payload: they are what you need to measure accuracy against ground truth later, and to explain a decision to an auditor.

The library is not included in the rpaframework package. Add rpaframework-decision to your conda.yaml or requirements.txt. For the Laya backend use rpaframework-decision[laya].

Backends

jev

TypeSafe’s hosted model. Needs api_key or TYPESAFE_API_KEY.

laya

Open-weight model running locally (CPU or CUDA). No data leaves the machine. Needs the laya extra.

replay

Replays answers from a saved audit log, for dry runs and tests.

callable

Any Python (state, questions) -> answers function.

Robot Framework example usage

*** Settings ***
Library    RPA.Robocorp.Vault
Library    RPA.Robocorp.WorkItems
Library    RPA.Decision    threshold=0.85    audit_log=output/decisions.jsonl

*** Tasks ***
Route incoming documents
    ${secrets}=    Get Secret    TypeSafe
    Authorize To Jev    api_key=${secrets}[api_key]
    For Each Input Work Item    Route One Document

*** Keywords ***
Route One Document
    ${text}=    Get Work Item Variable    document_text
    ${decision}=    Decide    ${text}
    ...    question=What kind of document is this?
    ...    options=invoice=A request for payment, credit_note=A credit against an invoice, reminder, other
    Log    Routed as ${decision.choice} (${decision.confidence})
    IF    ${decision.confidence} < 0.85
        Set Work Item Variable    needs_review    ${True}
    END
    Set Work Item Variable    document_type    ${decision.choice}
    Set Work Item Variable    decision_audit    ${decision.to_dict()}
    Save Work Item

Reply only when it is safe
    ${verdict}=    Check    ${email_body}
    ...    question=Is this a routine request that can be answered with a template?
    ...    threshold=0.9
    IF    ${verdict}
        Send Template Reply
    ELSE
        Create Output Work Item    variables={"reason": "manual"}
    END

Escalate instead of guessing
    TRY
        ${choice}=    Decide Or Escalate    ${ticket}
        ...    question=Which team owns this ticket?
        ...    options=billing,technical,sales
        ...    threshold=0.9
    EXCEPT    LowConfidenceError*    type=glob
        Escalate To Human    ${ticket}
    END

Python example usage

from RPA.Decision import Decision, LowConfidenceError

decide = Decision(backend="jev", api_key="...", threshold=0.85)

result = decide.decide(
    invoice_text,
    question="What kind of document is this?",
    options={"invoice": "A request for payment", "credit_note": None, "other": None},
)
print(result.choice, result.confidence, result.probabilities)

try:
    team = decide.decide_or_escalate(ticket, "Which team owns this?", ["billing", "tech"])
except LowConfidenceError as err:
    route_to_human(ticket, err.result)

# Local, offline alternative:
local = Decision(backend="laya", device="cpu")
ok = local.check(email, "Does the sender ask for a refund?")
if ok:
    ...

Design notes

  • Keep questions about the state and name the field you mean when the state is a dictionary: "Does `body` ask for a refund?".

  • Give options a one-line description when labels are ambiguous; the description is part of the prompt the model sees.

  • Decision models are strong on recognition and weak on arithmetic. Compute totals in Python; ask the model whether a document is an invoice, not whether its total is correct.

  • Treat the threshold as a business decision, tune it per use case from the audit log, and store the chosen value next to the decision.

ROBOT_LIBRARY_DOC_FORMAT = 'REST'
ROBOT_LIBRARY_SCOPE = 'GLOBAL'
ask_questions(state: str | Dict[str, Any], questions: Dict[str, Dict[str, Any]]) → Dict[str, ChoiceResult | CheckResult | ScoreResult]

Ask several questions about the same state in one call.

Batching questions that share a state is cheaper and faster than separate calls. Each question is a dictionary with type (choice / check / score), question, and options (choice) or levels (score). Native backend question objects (type: noul with instructions) are also accepted as-is.

Returns:

Dictionary keyed by question id with the matching result type.

Robot Framework example:

&{type}=    Create Dictionary    type=choice
...    question=What kind of document is this?    options=invoice,other
&{urgent}=    Create Dictionary    type=check
...    question=Does the document demand payment within 7 days?
&{questions}=    Create Dictionary    doc_type=${type}    urgent=${urgent}
${answers}=    Ask Questions    ${text}    ${questions}
Log    ${answers}[doc_type][choice] is urgent: ${answers}[urgent][result]
authorize_to_jev(api_key: str, model: str | None = None, base_url: str | None = None, timeout: float = 30.0, retries: int = 2) → None

Use TypeSafe’s hosted Jev model (or any /v1/systemone compatible endpoint).

Parameters:
  • api_key – TypeSafe API key. Keep it in Vault, not in the robot.

  • model – Model alias, default jev-latest.

  • base_url – Override the API host, e.g. for a logging proxy.

  • timeout – HTTP timeout in seconds.

  • retries – Retries on 429/5xx and connection errors.

Robot Framework example:

${secrets}=    Get Secret    TypeSafe
Authorize To Jev    api_key=${secrets}[api_key]
property backend: DecisionBackend
check(state: str | Dict[str, Any], question: str, threshold: float = 0.5, criteria_true: str | None = None, criteria_false: str | None = None) → CheckResult

Answer a yes/no question with a calibrated probability.

Parameters:
  • state – The text or dictionary to look at.

  • question – A proposition that is either true or false.

  • threshold – Probability at or above which result is True. Raise it (0.9) when a false positive is expensive, lower it when a false negative is.

  • criteria_true – Optional description of what counts as yes.

  • criteria_false – Optional description of what counts as no.

Returns:

CheckResult. ${result} is truthy in IF; ${result.probability} is the raw probability.

Robot Framework example:

${safe}=    Check    ${email}
...    question=Is this a routine request that a template answers fully?
...    threshold=0.9
IF    ${safe}    Send Template Reply    ${email}
classify_items(items: Sequence[str | Dict[str, Any]], question: str, options: str | Sequence[str] | Dict[str, str | None], threshold: float | None = None) → List[ChoiceResult]

Run Decide over a list of states and return one result per item.

Items are decided one at a time so that a single failure does not lose the batch; the audit log records every item.

Robot Framework example:

${results}=    Classify Items    ${subjects}
...    question=Is this email subject about an order or something else?
...    options=order,other
FOR    ${subject}    ${result}    IN ZIP    ${subjects}    ${results}
    Log    ${subject} -> ${result.choice}
END
clear_decision_audit_log() → int

Forget the in-memory audit records. Returns how many were cleared. The JSON Lines file, if any, is left untouched.

decide(state: str | Dict[str, Any], question: str, options: str | Sequence[str] | Dict[str, str | None], threshold: float | None = None) → ChoiceResult

Pick one option from a closed set.

Parameters:
  • state – The text or dictionary the model should look at. A dictionary is sent as structured JSON; refer to its keys in the question with backticks, e.g. "What is `subject` about?".

  • question – The question to answer, phrased so that exactly one of the options is the right answer.

  • options – The allowed answers. Accepts a list of labels, a dictionary {label: description} (description may be None), or a string: billing,technical,sales or billing=Payments and refunds,technical=Bugs and outages,other.

  • threshold – Only used for the accepted flag in the audit record; this keyword never fails on low confidence.

Returns:

ChoiceResult with choice, confidence and probabilities. Use ${result.choice} in Robot Framework; ${result} alone logs as the chosen label.

Robot Framework example:

${result}=    Decide    ${email_text}
...    question=Which team should handle this email?
...    options=billing=Invoices and refunds,technical=Bugs and outages,sales
IF    ${result.confidence} >= 0.9
    Assign To    ${result.choice}
ELSE
    Log    Unsure: ${result.probabilities}    level=WARN
END
decide_or_escalate(state: str | Dict[str, Any], question: str, options: str | Sequence[str] | Dict[str, str | None], threshold: float | None = None) → ChoiceResult

Like Decide, but fails with LowConfidenceError when the confidence is below threshold (default: the library threshold).

The error message carries the choice, the confidence and the threshold. The exception object (in Python) carries the full result as err.result.

Robot Framework example:

TRY
    ${type}=    Decide Or Escalate    ${text}
    ...    question=What kind of document is this?
    ...    options=invoice,credit_note,other
    ...    threshold=0.9
EXCEPT    LowConfidenceError*    type=glob    AS    ${error}
    Log    ${error}    level=WARN
    Create Output Work Item    variables={"route": "manual_review"}
END
get_decision_audit_log() → List[Dict[str, Any]]

Return all decisions made so far as a list of dictionaries.

Each record contains the question, options, answer, confidence, probabilities, threshold, whether it was accepted, latency, model and a digest of the state (never the state itself).

rank_options(state: str | Dict[str, Any], question: str, options: str | Sequence[str] | Dict[str, str | None]) → List[Tuple[str, float]]

Return all options ordered from most to least probable.

Handy when the top choice is not enough, e.g. to try candidates in order until one works, or to offer a human the best three.

Returns:

List of (label, probability) pairs.

Robot Framework example:

${ranked}=    Rank Options    ${page_text}
...    question=Which link leads to the invoices page?
...    options=${link_texts}
FOR    ${label}    ${prob}    IN    @{ranked}
    Log    ${label}: ${prob}
END
require_confidence(result: ChoiceResult | CheckResult | ScoreResult, threshold: float | None = None) → None

Fail with LowConfidenceError unless the given result meets the threshold. Lets you make the decision first, log it, and gate later.

Parameters:
  • result – A result from Decide, Check or Score.

  • threshold – Defaults to the library threshold.

save_decision_audit_log(path: str) → str

Write the in-memory audit records to path as JSON Lines.

Returns:

The path written.

Robot Framework example:

Save Decision Audit Log    ${OUTPUT_DIR}${/}decisions.jsonl
score(state: str | Dict[str, Any], question: str, levels: str | Sequence[str]) → ScoreResult

Place the state on an ordered rubric of 2 - 10 levels.

Parameters:
  • state – The text or dictionary to look at.

  • question – What is being rated.

  • levels – Level labels from lowest to highest, as a list or a comma-separated string. Keep the labels descriptive; they are the rubric the model applies.

Returns:

ScoreResult with the weighted score, the most probable integer level, its label and per-level probabilities.

Robot Framework example:

${tone}=    Score    ${complaint}
...    question=How upset is the customer?
...    levels=Calm,Irritated,Angry,Threatening to churn
IF    ${tone.level} >= 2    Flag For Account Manager
set_decision_backend(backend: str | DecisionBackend | Callable, **options: Any) → None

Select the decision model backend used by all other keywords.

Parameters:
  • backend – jev, laya, replay, a backend instance, or a Python callable (state, questions) -> answers.

  • options – Backend-specific options. jev: api_key, model, base_url, timeout, retries. laya: model, device, preload. replay: path.

Robot Framework example:

Set Decision Backend    laya    device=cpu
Set Decision Backend    replay    path=recorded/decisions.jsonl
set_decision_threshold(threshold: float) → float

Change the default confidence threshold. Returns the previous value.

Parameters:

threshold – A value between 0 and 1.

use_laya(model: str | None = None, device: str | None = None, preload: bool = True) → None

Use the open-weight Laya model locally. Nothing leaves the machine.

Parameters:
  • model – Hugging Face repo or local path, default convaiinnovations/laya.

  • device – cpu or cuda; auto-detected when omitted.

  • preload – Keep checkpoints in memory between calls (recommended for robots that make many decisions).

Robot Framework example:

Use Laya    device=cpu

Result types

class RPA.Decision.ChoiceResult(choice: str, confidence: float, probabilities: ~typing.Dict[str, float], question: str = '', options: ~typing.List[str] = <factory>, model: str = '', backend: str = '', request_id: str | None = None, latency_ms: float = 0.0)

Answer to a choice question: one option picked from a closed set.

Attr choice:

The winning option label.

Attr confidence:

How sure the model is about choice (0.0 - 1.0).

Attr probabilities:

Probability for every option, keyed by label.

Attr question:

The instructions that were asked.

Attr options:

The option labels that were allowed.

Attr model:

Resolved model identifier reported by the backend.

Attr backend:

Name of the backend that produced the answer.

Attr request_id:

Backend request id, when the backend reports one.

Attr latency_ms:

Wall-clock time of the backend call.

ranked() → List[Tuple[str, float]]

Options sorted from most to least probable.

class RPA.Decision.CheckResult(probability: float, result: bool, threshold: float, question: str = '', model: str = '', backend: str = '', request_id: str | None = None, latency_ms: float = 0.0)

Answer to a yes/no question (a noul in TypeSafe’s terminology).

Attr probability:

Calibrated probability that the proposition is true.

Attr result:

True when probability >= threshold.

Attr threshold:

The threshold that was applied to produce result.

property is_uncertain: bool

True when the probability sits in the 0.35 - 0.65 band.

class RPA.Decision.ScoreResult(score: float, level: int, label: str, confidence: float, probabilities: Dict[int, float], legend: Dict[int, str], question: str = '', model: str = '', backend: str = '', request_id: str | None = None, latency_ms: float = 0.0)

Answer to a score question: a position on an ordered rubric.

Attr score:

Probability-weighted level, may fall between levels (e.g. 1.4 on a 0 - 2 rubric).

Attr level:

The most probable integer level.

Attr label:

Human-readable label of level.

Attr confidence:

Confidence in level.

Attr probabilities:

Probability per level, keyed by integer level.

Attr legend:

Mapping from level to its label.

to_dict() → Dict[str, Any]

Return the result as a plain, JSON-serializable dictionary.

exception RPA.Decision.LowConfidenceError(result: ChoiceResult, threshold: float)

A decision was made, but its confidence is below the required threshold.

The exception carries the full result so an EXCEPT branch in Robot Framework (or a Python except) can still inspect what the model thought, log it, or route the item to a human.

Backends

Decision-model backends.

Every backend speaks the same question schema, which follows TypeSafe’s System One API because it is the de-facto convention that open models (Laya) have adopted as well:

questions = {
    "doc_type": {
        "type": "choice",
        "instructions": "What kind of document is `text`?",
        "criteria": {"invoice": "A request for payment", "other": None},
    },
    "urgent": {"type": "noul", "instructions": "Does `text` ask for urgent action?"},
    "tone": {
        "type": "score",
        "instructions": "How upset is the writer?",
        "criteria": ["Neutral", "Irritated", "Furious"],
    },
}

and every backend returns answers in the same normalized shape:

{
    "doc_type": {"type": "choice", "choice": "invoice", "confidence": 0.93,
                 "probabilities": {"invoice": 0.93, "other": 0.07}},
    "urgent":   {"type": "noul", "noul": 0.12},
    "tone":     {"type": "score", "score": 0.4, "confidence": 0.7,
                 "probabilities": {"0": 0.7, "1": 0.2, "2": 0.1},
                 "legend": {"0": "Neutral", "1": "Irritated", "2": "Furious"}},
}

The library never depends on a vendor SDK; the hosted backend is a thin requests call so the dependency footprint stays small and the request can be audited byte for byte.

class RPA.Decision.backends.CallableBackend(func: Callable[[str | Dict[str, Any], Dict[str, Dict[str, Any]]], Any], model: str = 'callable')

Wrap any Python callable (state, questions) -> answers.

Useful for tests, for deterministic fallbacks, and for plugging in a model the library does not know about yet without touching the library.

class RPA.Decision.backends.DecisionBackend

Interface every backend implements.

close() → None

Release any resources (HTTP sessions, loaded models).

class RPA.Decision.backends.JevBackend(api_key: str | None = None, model: str | None = None, base_url: str | None = None, timeout: float = 30.0, retries: int = 2, session: Any = None)

TypeSafe AI’s hosted Jev model, via POST /v1/systemone.

Works with any service exposing the same endpoint, which is why base_url is configurable (a self-hosted gateway, a proxy that adds logging, a mock server in CI).

close() → None

Release any resources (HTTP sessions, loaded models).

class RPA.Decision.backends.LayaBackend(model: str | None = None, device: str | None = None, preload: bool = True, **router_kwargs: Any)

Laya, the open-weight decision model, running locally.

Requires the laya extra (pip install rpaframework-decision[laya]). The model is loaded lazily on first use, and kept in memory for the lifetime of the library (which is GLOBAL scope by default).

class RPA.Decision.backends.ReplayBackend(path: str)

Replay answers from a recorded audit log (JSON Lines) for dry runs.

Each line is an AuditRecord; answers are matched by question text + options, in order of appearance.