Adaptation src/pipeline/adaptation.py
When governance decides to intervene, the adaptation module resolves what to show. The governance decision specifies a level and a subtype. The adaptation module finds the right content for that level in the participant's language, delivers it, and records what happened.
Intervention levels
| Level | Subtype | What the participant sees | When it fires |
|---|---|---|---|
| L1 | tooltip | A small contextual hint near the relevant element. Brief, non-intrusive. Can be dismissed with one tap. | MODERATE state with mild struggle (R4). Locator nudge after time threshold in template. |
| L2a | guided prompt | A more prominent help card with specific instructions for the current step. Appears over the content. | LOW state, explicit help request, abandonment risk. Most common intervention type. |
| L2b | reassurance / rest | An encouraging message ("You're doing well, take a breath") or a rest prompt ("It's okay to take a short break"). | Abandonment risk, fatigue signals, CAPTCHA entry, sustained OVERLOAD with high fatigue. |
| L3 | walkthrough | One to two sentences generated by Mistral LLM, tuned to the participant's readiness state, device, and step context. Shown as a dark overlay; auto-dismisses after 45 seconds. A typing indicator appears while the LLM generates the response. | OVERLOAD with manageable fatigue (R7), escalation from a prior L2 that was ignored, explicit help in OVERLOAD. |
Content resolution — what gets shown
Resolution differs by level. L1 and L2 use the scripted content library; L3 always uses the LLM.
L1 and L2 — scripted cascade, first match wins:
1. step-specific content in the participant's language ← most tailored
2. step-specific content in Kiswahili
3. generic content in the participant's language
4. generic content in Kiswahili
5. scripted fallback ← guaranteed delivery
L3 — LLM-first, scripted fallback only on failure:
1. Mistral LLM (streaming, 8s timeout) ← primary
2. scripted fallback ← on timeout or API error
When L3 fires, the overlay shows a typing indicator immediately while Mistral generates a one-to-two sentence response tuned to the participant's current readiness state, device type, and step context. If Mistral times out, a scripted fallback resolves the overlay. routing_decision is logged in all cases (llm_generated, llm_timeout, or llm_error).
For explicit help requests (button press), L3 calls the LLM synchronously within the request and returns the response directly — no pub-sub involved.
Language adaptation
Language adaptation fires as a special case — it changes the content language mid-session, independent of intervention level. Two triggers:
language_mismatch = true— AGS detected that the participant is interacting with a site in a different language than their preference. Common on government sites that default to English.zoom_event_count ≥ 2on mobile — Two or more zoom-in events on the same page is a readability signal. The participant is struggling to read the content. Switching to a larger-text, simpler-language variant is prioritised.
Tracking what happened after an adaptation
Every adaptation delivery writes an ADAPTATION_ACTION row. The next event from the participant is the outcome — did errors decrease? Did the step complete? AGS compares the pre- and post-adaptation signals to classify the outcome.
fatigue_score and frustration values in Redis — a reward signal that reduces the urgency of the next intervention. error_rate_step is never halved — that's a historical record, not a state signal.Adaptation failures — when help makes things worse
An ADAPTATION_FAILURE row is written whenever a worsened outcome is detected. Four failure types, each with different implications:
| Failure type | What happened | What AGS does |
|---|---|---|
| Ignored | The participant dismissed the adaptation within less than 500ms (help_dwell very low) — almost certainly didn't read it. | Logs the failure. Governance will factor in that this participant has a pattern of ignoring this type of content. |
| Worsened | Measurable worsening after the adaptation: error rate increased, or the step took significantly longer after the overlay appeared. | Writes ADAPTATION_FAILURE, sets 60-second passive_help_only cooldown in Redis. Only the Help button can fire during this window. |
| Conflict | The adaptation fired into a conflicting state — e.g., a guided prompt delivered while the participant was actively completing the step successfully. | Logs the conflict. A signal that the governance timing was off — the state that triggered the rule had already resolved by delivery time. |
| Repeated_Ignored | The same level of adaptation has been ignored ≥ 2 times in this session. | Governance escalates on the next trigger — if L2 has been ignored twice, the next intervention will be L3. If L3 is being ignored, the worsened cooldown fires. |
Adaptation and episode context
Within a multi-episode session, each new episode resets current_step_id in Redis to the scenario's first_step. This means governance and adaptation content resolution start fresh at the correct step for each episode — step-specific content for whatsapp_open_sarah is delivered correctly whether it's Episode 1 or Episode 3.
The only cross-episode signal is ep_readiness_floor — the minimum readiness seen during the episode, written by the events API after every pipeline run. This value flows into the episode performance score at episode completion. It does not affect adaptation decisions during the episode; it is purely a scoring input for the sequencer.
Adaptation counters in Redis (dismissal count, passive_help_only flag, worsened cooldown) are session-scoped, not episode-scoped. A dismissal in Episode 1 counts toward the DISMISS_LIMIT in Episode 2. This is intentional: the dismissal pattern reflects participant behaviour in this session, not just this scenario.