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 Escalateone option + probability for every option
yes/no
Checkprobability that the proposition is true
score
Scoreposition 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
rpaframeworkpackage. Addrpaframework-decisionto your conda.yaml or requirements.txt. For the Laya backend userpaframework-decision[laya].Backends
jevTypeSafe’s hosted model. Needs
api_keyorTYPESAFE_API_KEY.layaOpen-weight model running locally (CPU or CUDA). No data leaves the machine. Needs the
layaextra.replayReplays answers from a saved audit log, for dry runs and tests.
callable
Any Python
(state, questions) -> answersfunction.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, andoptions(choice) orlevels(score). Native backend question objects (type: noulwithinstructions) 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/systemonecompatible 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
resultisTrue. 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 inIF;${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
Decideover 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 beNone), or a string:billing,technical,salesorbilling=Payments and refunds,technical=Bugs and outages,other.threshold – Only used for the
acceptedflag in the audit record; this keyword never fails on low confidence.
- Returns:
ChoiceResultwithchoice,confidenceandprobabilities. 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 withLowConfidenceErrorwhen the confidence is belowthreshold(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
LowConfidenceErrorunless the given result meets the threshold. Lets you make the decision first, log it, and gate later.- Parameters:
result – A result from
Decide,CheckorScore.threshold – Defaults to the library threshold.
- save_decision_audit_log(path: str) str
Write the in-memory audit records to
pathas 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:
ScoreResultwith the weightedscore, the most probable integerlevel, itslabeland per-levelprobabilities.
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 –
cpuorcuda; 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:
Truewhenprobability >= threshold.- Attr threshold:
The threshold that was applied to produce
result.
- property is_uncertain: bool
Truewhen 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.4on 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
EXCEPTbranch in Robot Framework (or a Pythonexcept) 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_urlis 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
layaextra (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.