Concepts
The whole Viora data model lives on top of seven primitives. If you understand these, you understand everything else.
Client
A client is your company — the tenant integrating the Viora API. Each client has:
- A
slug(short identifier, e.g.acme-health) - A
name(display name) - An
api_key(vrk_…) used in theAuthorizationheader - A
planand optionalbilling_email
You'll be issued exactly one active API key. Your traffic — sessions, end-users, messages — is scoped to your client and invisible to other tenants.
Created by: a Viora admin. Either via the admin UI at
/admin/clientsor the CLIuv run python -m viora.cli clients create <slug> <name>. You receive the key plaintext once at creation time.
End-user
An end-user is one of your users — the human whose conversations you're scoring. You identify them with an opaque external_id (anything stable: your internal user ID, a hash, a UUID). We never see or care what's behind the ID.
- Unique per client (
(client_id, external_id)is the primary lookup) - Tracks
first_seen_at,last_seen_at - Carries a long-term LBRS (see below) — what the user looks like across all of their sessions
Created by: implicitly. The first time you reference an
external_id(inPOST /v1/sessions), we create the end-user record under your client. You never call a "create end-user" endpoint.
Session
A session is one logical conversation — typically one continuous chat between your bot and one end-user. You decide where sessions begin and end. Common patterns:
-
One session per "conversation" in your UI
-
One session per support ticket
-
A new session every N hours of inactivity
-
A single ever-growing session per end-user (rarely a good idea)
-
Identified internally by
id(integer) and externally byexternal_id(your string, optional but recommended for idempotent reposting) -
Belongs to one client and one end-user
-
Has a current
r_level(R0/R1-mid/R1-high/R2) andsrs(the rolled-up session risk score) -
Has a
status(active/closed)
Created by:
POST /v1/sessionswithend_user_external_id(required). The request is idempotent on(client_id, external_id)— reposting the same body returns the existing session.
Message
A message is one turn — a single utterance from user, assistant, or system.
- Has an
ordinal(zero-based position in the session) - Has
content(the text) - After scoring, carries:
prs— per-turn risk score in[0, 1]scores— continuous per-signal scores (the 10 signals)r_level— categorical bucket on PRSstructured— 5-block schema for user turns (intent, plan, means, timeframe, emotional_state, protective_factors, co_signals, primary_risk_signals)forced_by_imminence—truewhen structured fields forced R2 regardless of continuous PRS
Created by:
POST /v1/sessions/{id}/messages. Each call scores the conversation-through-this-turn and returns the result inline.
PRS — Per-turn Risk Score
The per-message risk score. Computed as the peak of the continuous signal scores, lifted higher when structured imminence fields trigger.
Range: [0, 1]. Threshold bands map to R-levels (R0 < 0.3, R1-mid < 0.6, R1-high < 0.8, R2 ≥ 0.8).
The imminence override forces R2 regardless of PRS if structured fields show clear imminent risk (intent + plan/means/timeframe).
SRS — Session Risk Score
The per-session aggregate. Computed as max(peak PRS, recency-weighted-sum) over all turns in the session. Combines:
- Peak: the worst turn so far
- Recency-weighted sum: an EWMA over the PRS trajectory, so a single old high turn fades but sustained moderate distress compounds
The session-level R-level applies a sticky-floor: it cannot auto-downgrade. To allow a step-down by one level, the latest turn must show structured de-escalation evidence (non-empty protective_factors, intent drop ≥ 0.3, plan not specific).
LBRS — Long-Baseline Risk Score (per end-user)
The per-end-user aggregate across sessions. An EWMA over session SRSs with a 30-day half-life, plus a consecutive_r1_plus_sessions counter.
This is what surfaces patterns no single session catches: someone whose individual sessions look mild but who hits R1-mid every Sunday for two months has an elevated LBRS even with no acute crisis turn.
How they connect
Client ─── EndUser ─── Session ─── Message
│ │ │ │
│ ▼ ▼ ▼
│ LBRS SRS PRS + structured
│
└── UsageEvent (one per message scored, per session created, per ack)
Every API call is rooted in a Client (via your API key). Every Session is rooted in a (Client, EndUser). Every Message is rooted in a Session.
Usage events sit beside the conversation data — they're what we count for billing.
What we don't model
- Replies. Viora doesn't generate bot replies. Your product owns the assistant side; we score what flows through it.
- Demographics. We never ask for age, gender, location, or any other end-user attribute. Whatever opaque
external_idyou send is all we have. - Cross-client signal. A user with the same
external_idunder two different clients is two different end-users to us. No fingerprinting.