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 the Authorization header
  • A plan and optional billing_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/clients or the CLI uv 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 (in POST /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 by external_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) and srs (the rolled-up session risk score)

  • Has a status (active / closed)

Created by: POST /v1/sessions with end_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 PRS
    • structured — 5-block schema for user turns (intent, plan, means, timeframe, emotional_state, protective_factors, co_signals, primary_risk_signals)
    • forced_by_imminencetrue when 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_id you send is all we have.
  • Cross-client signal. A user with the same external_id under two different clients is two different end-users to us. No fingerprinting.