Decisions API
Categorical decision requests, results, and explicitly declared workflows.
#Decisions API
Categorical decisions and explicitly declared workflows from maivn.decisions.
This reference describes the prepared decision-feature release. Use an SDK build that includes this module and a platform deployment with the decisions API available. Publishing documentation does not activate these features.
The SDK uses your ordinary MAIVN_API_KEY and the single MAIVN_BASE_URL (default https://api.maivn.io). The platform selects the decision model; there is no developer decision-provider key, model selector or separate service origin. Declaring a request, stage or decorator performs no inference.
See the Decisions guide for design and privacy rules and Decision examples for application patterns. The Python blocks below reuse the quick-start definitions in order.
#Quick start
maivn.decisions answers categorical questions through the mAIvn platform and falls back to your own baseline when a decision is unavailable, refused or not confident enough. The SDK never calls a decision model directly: requests go to POST /v1/decisions with the same API key and MAIVN_BASE_URL as every other call. The platform chooses the decision model, shields explicitly marked private values and bills the call to your project.
from maivn.decisions import (
DecisionAnswer, DecisionChoice, DecisionClient, DecisionConfig, DecisionRequest,
)
question = DecisionRequest(
name='urgent',
state='Public bulletin: planned API maintenance on Saturday.',
instructions='Does this bulletin need immediate action?',
privacy='public',
choices=(
DecisionChoice(key='yes', description='Immediate action'),
DecisionChoice(key='no', description='No immediate action'),
),
)
async def baseline(request: DecisionRequest) -> DecisionAnswer:
return DecisionAnswer(value='yes', provider='local', token_category=None)
decisions = DecisionClient(
fallback=baseline,
config=DecisionConfig(min_confidence=0.8),
authorize=lambda request: request == question,
)
result = decisions.decide(question)
print(result.value, result.source, result.primary.status)Only requests declared privacy='public' and approved by authorize are sent. Mark private values inside otherwise public text with DecisionRequest(..., private_data={'customer_name': 'Priya Raman'}): the platform replaces them with placeholders before the decision model sees anything. A placeholder verdict is resolved locally and must still match a declared choice. Raw marked values reach the platform shield, never the decision model. Plain unmarked text receives no automatic redaction. Your fallback receives the original request and must enforce your application's privacy policy. Pass client=Client(...) to reuse an existing client's settings.
#Decision token usage
Decision workflows report two separate categories: decision for platform decisions and llm for ordinary model invocations, including model fallbacks. Use the additive token_usage_by_category property on a decision result, builder outcome, argument preparation, matching result, or decorated call. For a list of decisions, use:
from maivn.decisions import summarize_decision_usage
usage = summarize_decision_usage([result])
print(usage.decision.input_tokens)
print(usage.llm.input_tokens)Native batch followers do not count the shared receipt again. Missing counts remain None; known_input_tokens and known_output_tokens retain the observed subtotal. A skipped agent invocation contributes zero invocation tokens while its decision receipts remain visible. Declared local fallbacks use DecisionAnswer(..., token_category=None); a decision-model backup declares token_category='decision'. Ordinary model fallbacks default to llm.
These counts are not interchangeable billing units. LLM invocation counts mirror the existing public SDK input/output surface; provider totals and cache counts remain available through provider_usage. Price each capability with its applicable provider tariff and retain costs separately. The existing token_usage property keeps its original behavior.
#Decision reference and workflows
All symbols below are imported from maivn.decisions. These workflows are opt-in; creating a request, builder or decorator performs no inference. Synchronous execution methods run outside an active event loop; use their async counterparts inside async applications. The examples below reuse question, baseline and decisions from the Decisions example. They make platform calls when executed unless a feature is explicitly disabled.
#Requests, routing and results
Request and answer models are strict, reject extra fields and are immutable. private_data accepts a mapping or tuple of key/value pairs: up to 64 unique keys, each following the choice-key bounds, with string values of 1–4,096 characters. Mappings become sorted immutable pairs; request.private_values returns a detached dictionary.
DecisionClient(fallback=..., config=..., authorize=..., client=None) requires an async fallback returning DecisionAnswer and a synchronous authorization callback. Only an exact True approves a complete request. Authorization errors, private/unknown privacy, or a disabled client prevent primary dispatch. Approval is a local export permission, not a substitute for declaring private values. No vendor key or decision-model identifier is needed. An existing Client can supply the authenticated platform transport.
Use decide(request) / await adecide(request) for one question. Confidence below the threshold, refusal, malformed or out-of-choice answers, timeout and platform failure use the fallback at most once per question. Its returned value must be a declared choice. Invalid fallback answers raise ValueError; fallback exceptions propagate. Cancellation propagates rather than starting another route. Request validation errors also propagate. Your fallback is not subject to the primary attempt's timeout.
with_fallback(callback) creates a client sharing the route, authorization and configuration with a different fallback. The original client is unchanged.
#Ordered and native batches
batch_requests = (question, question.model_copy(update={'name': 'urgent_again'}))
batch_client = DecisionClient(
fallback=baseline, config=DecisionConfig(),
authorize=lambda request: request in batch_requests,
)
results = batch_client.batch(batch_requests, max_concurrency=2, native_batch=True)
print([result.source for result in results])batch / abatch return results in input order. max_concurrency=4 bounds outstanding work by default and must be a positive integer. native_batch=False runs scalar decisions; True combines questions with identical state and private_data, splitting at max_batch_questions. native_batch_question_limit reports that limit (one when the primary is disabled). Approve every full request in your authorization policy. If any member of a shared packet is not approved, that packet falls back without dispatch. Private or unknown requests fall back individually. On caller cancellation or failure, outstanding batch tasks are cancelled and drained.
A native packet has one token receipt. The first member owns it; followers expose usage_scope='batch', the batch size and usage_owner=False, without duplicating usage. Aggregate the whole result collection to retain the owned receipt.
#Before and after an invocation
Agent.decide and Swarm.decide return a DecisionInvocationBuilder. The same operation composes with their structured-output, event and follow-up builders. Stage names and question labels start with a letter and contain letters, digits, underscores, dots or hyphens, up to 128 characters; stage names must be unique.
from maivn import Agent
plan = (
Agent(name='Bulletin assistant')
.decide(
'triage', client=decisions, questions={'urgent': question},
require={'urgent': 'yes'},
)
.decide('review', questions={'urgent': question}, at='after')
)
outcome = plan.invoke('Summarize the public maintenance bulletin.')
print(outcome.status, outcome.skipped_by)
if outcome.response is not None:
print(outcome.response.response)Each .decide(name, client=..., questions=..., at='before', require=None, max_concurrency=4, native_batch=False) stage has 1–32 questions. Subsequent stages may omit client to reuse the previous stage's client. DecisionQuestion values are requests or synchronous factories taking DecisionContext. Each factory must explicitly construct its own request. Context contains the original input (also original_input), output (absent before invocation), and prior nested decisions[stage][question]. Context does not authorize exporting input or output.
A require gate skips invocation only when an accepted primary answer differs from the required choice. Any fallback preserves normal invocation, even when its value differs. After stages annotate the completed response and cannot have require. .map_input(mapper) explicitly changes invocation input after before stages; without it, decisions do not change messages.
invoke / ainvoke return DecisionInvocationResult: status is completed or skipped, response is an InvokeResponse or None, decisions is the nested result mapping, and skipped_by identifies a blocking stage. batch / abatch accept an iterable of inputs and max_concurrency=4, preserve order and share a bound across decisions and invocations. Invocation keyword arguments pass through to the bound scope.
.structured_output(Model), .events(include=None, exclude=None, on_event=None, auto_verbose=True) and .allow_followup_questions(**kwargs) retain the decision stages and configure the existing invocation wrappers.
stream / astream forward ordinary invocation events and add decision.stage.started, decision.stage.completed and decision.plan.completed. The final DecisionStreamEvent.outcome holds the typed result locally and is excluded from serialized event data; read it before discarding the event.
from maivn.decisions import DecisionStreamEvent
for event in plan.stream('Summarize the public maintenance bulletin.'):
if isinstance(event, DecisionStreamEvent) and event.outcome is not None:
print(event.outcome.status)#Typed argument preparation
from maivn.decisions import DecisionArgument, DecisionArgumentPreparer
prepared = DecisionArgumentPreparer(decisions).prepare([
DecisionArgument(
name='notify', question=question, values={'yes': True, 'no': False},
),
])
# Preserve the existing value when the optional decision falls back.
notify = prepared.values.get('notify', True)
print(notify, prepared.decisions['notify'].source)DecisionArgument(name, question, values) maps every choice exactly once to a boolean or string; its name must be a valid Python identifier, excluding keywords. DecisionArgumentPreparer.prepare / aprepare accept unique named arguments, max_concurrency=4 and native_batch=False. DecisionArgumentPreparation.values contains only accepted primary mappings; fallbacks leave the field absent so your existing argument preparation can continue. .decisions retains every result. It does not register a tool or automatically inject arguments into one.
#Function and local tool dependencies
from maivn.decisions import (
DecisionResults, DecisionStep, decision_flow, depends_on_decision,
)
@depends_on_decision(
client=decisions,
decisions=[DecisionStep(
'triage', questions={'urgent': question}, require={'urgent': 'yes'},
)],
arg_name='decision_results',
)
def notify_bulletin(message: str, decision_results: DecisionResults) -> str:
return message
call = decision_flow(notify_bulletin).invoke({'message': 'Maintenance bulletin'})
print(call.status, call.value)depends_on_decision(*, client, decisions, arg_name=None, max_concurrency=4, state=None, native_batch=False) declares metadata. Direct Python calls keep their original behavior; use decision_flow(function) (a DecisionFunction) or register the decorated function as an SDK-local tool to execute its dependencies. A tool's injected decision parameter is hidden from its public input schema.
DecisionStep(name, *, questions, after=(), require=None, client=None, max_concurrency=4) declares a stage. after names a predecessor or tuple of predecessors within the same decorator group. Unknown predecessors and cycles are rejected; ready steps can run concurrently. A step may override its client. Factories receive a local snapshot of bound function arguments, including defaults, through DecisionContext.input. The optional synchronous state mapper narrows this snapshot once per factory stage; its result must support deep copying. A factory explicitly chooses which content goes into its exported request before authorization. Static requests need no input snapshot. arg_name injects nested results into a parameter annotated exactly as DecisionResults. The caller must not supply that hidden parameter.
Managed invoke / ainvoke accept a mapping of function arguments; batch / abatch accept iterables of mappings with max_concurrency=4. DecisionCallResult contains status (completed or skipped), the native return value (or None when skipped), nested decisions, skipped_by and category token usage. Gates apply the same accepted-primary rule as invocation builders. Fallbacks preserve function execution; errors from the function propagate. DecisionResults is the mapping type stage -> question -> DecisionResult.
#Optional candidate matching
Matching operates on an already authorized, retrieved candidate set. It neither retrieves records nor grants access to them. It is disabled by default, with no primary call while disabled.
from maivn.decisions import (
DecisionCandidateMatcher, DecisionMatchCandidate, DecisionMatchConfig,
DecisionMatchRequest, candidate_match_questions,
)
match_request = DecisionMatchRequest(
query='When is maintenance?',
candidates=(DecisionMatchCandidate(key='bulletin', text=question.state),),
baseline_keys=('bulletin',),
privacy='public',
)
# Review the complete generated requests before enabling platform matching.
approved = candidate_match_questions(match_request)
match_client = DecisionClient(
fallback=baseline, config=DecisionConfig(),
authorize=lambda request: request in approved,
)
matcher = DecisionCandidateMatcher(match_client, DecisionMatchConfig())
match_result = matcher.match(match_request)
print(match_result.selected_keys, match_result.reason) # ('bulletin',), disabledDecisionMatchCandidate has a unique local key (1–512 characters) and excerpt (1–1,200 characters). DecisionMatchRequest has query (1–16,384 characters), a tuple of up to 16 candidates, unique baseline_keys from those candidates, privacy='unknown', and max_results=5 (1–16). DecisionMatchConfig defaults to enabled=False, max_candidates=5 (1–16), max_state_characters=8192 (1–16,384 serialized characters), and timeout_seconds=1.0 (positive finite).
candidate_match_questions(request) constructs public-only questions for a local authorization allowlist; it raises ValueError for private/unknown input. Source keys and baseline keys stay local; the model sees synthetic candidate labels and bounded text. There is no private_data option on matching requests: use only approved public text, or retain the baseline.
match / amatch, and ordered batch / abatch(max_concurrency=4), return DecisionMatchResult(selected_keys, source, reason, decisions, latency_seconds). Source is primary or baseline; reasons are accepted, blocked, disabled, empty, oversized, rejected, timeout, unavailable or no_match. Only when every candidate question has an accepted primary yes or no answer, and at least one matches, can selection change. Matches preserve candidate order and are capped at max_results. Any unknown, refusal, malformed answer, timeout, privacy failure or no-match result preserves the exact baseline_keys, without truncating or reordering them. Matching uses its own local abstaining fallback rather than spending the decision client's caller fallback.
#Usage types and privacy guidance
TokenUsageCategory is decision or llm; TokenUsageByCategory holds one TokenCategoryUsage per category. Each category has input_tokens, output_tokens, total_tokens, known_input_tokens, known_output_tokens, unknown_input_calls and unknown_output_calls. Unknown totals remain None; known subtotals remain available. summarize_decision_usage(results) accepts a collection of decision results. All workflow outcomes expose token_usage_by_category; aggregating native receipts counts each owner once. These counters alone do not prove cost savings or production qualification.
Ordinary InvokeResponse.privacy_guidance contains advisory over-redaction suggestions when delivered by the platform. Guidance includes the actual delivered value; it does not change redaction or reveal model input. repr=False keeps it out of the response representation, but model_dump() and JSON serialization still include it. Display it only in a trusted developer interface; do not log or persist the guidance or serialize whole responses into diagnostics. Normalized SDK event builders do not guarantee a guidance field. Decision results have no privacy_guidance field.