jtlevine Claude Opus 4.6 (1M context) commited on
Commit
57d708b
·
1 Parent(s): 67187e4

Add AI trigger explanations behind EXPLANATION_MODE flag

Browse files

Wire TriggerExplainer into step_explain so explanation generation
can be switched from the default free template path to a Claude-
Haiku-4.5 path via EXPLANATION_MODE=ai. Default stays "template"
so nothing changes at runtime without an opt-in.

- config.py: read EXPLANATION_MODE env var (default "template")
- src/explanation/explainer.py: switch TriggerExplainer default
model from Sonnet to Haiku 4.5 (advisory rewriting is a
template-fill task, not reasoning)
- src/pipeline.py: dispatch Template vs Trigger in _step_explain
based on the flag; report mode and cost in StepResult details
- frontend/src/pages/Pipeline.tsx: add ScalingCostPanel showing
three tiers (current/pilot/state-wide) on the Architecture tab

The design point the panel makes: LLM cost is decoupled from
worker population because explanations fire at trigger events
(a handful per week), not per worker. Pilot at 106K workers
costs the same ~$0.05/wk as the demo; AI explanations on add
less than a dollar even at 10x coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

config.py CHANGED
@@ -6,6 +6,7 @@ Zones are real neighborhoods with outdoor worker populations
6
  vulnerable to heat stress.
7
  """
8
 
 
9
  from dataclasses import dataclass, field
10
 
11
  REGION_NAME = "East Africa"
@@ -14,6 +15,12 @@ LOCALE = "en-KE"
14
  CURRENCY = "USD"
15
  CURRENCY_SYMBOL = "$"
16
 
 
 
 
 
 
 
17
  # Data source configuration
18
  NASA_POWER_URL = "https://power.larc.nasa.gov/api/temporal/daily/point"
19
  OVERPASS_URL = "https://overpass-api.de/api/interpreter"
 
6
  vulnerable to heat stress.
7
  """
8
 
9
+ import os
10
  from dataclasses import dataclass, field
11
 
12
  REGION_NAME = "East Africa"
 
15
  CURRENCY = "USD"
16
  CURRENCY_SYMBOL = "$"
17
 
18
+ # Explanation mode for step 5 (EXPLAIN).
19
+ # "template" — free, instant, deterministic templates (default)
20
+ # "ai" — Claude Haiku 4.5 generates bilingual explanations per trigger event
21
+ # Scales with trigger events fired, not worker population.
22
+ EXPLANATION_MODE = os.environ.get("EXPLANATION_MODE", "template").strip().lower()
23
+
24
  # Data source configuration
25
  NASA_POWER_URL = "https://power.larc.nasa.gov/api/temporal/daily/point"
26
  OVERPASS_URL = "https://overpass-api.de/api/interpreter"
frontend/src/pages/Pipeline.tsx CHANGED
@@ -132,7 +132,75 @@ function ArchitectureDiagram() {
132
  )
133
  }
134
 
135
- // (Build Your Own content is now inline in the main component)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  // ---------------------------------------------------------------------------
138
  // Main Component
@@ -289,6 +357,7 @@ export default function Pipeline() {
289
  {activeTab === 'architecture' && (
290
  <div className="animate-tab-enter">
291
  <ArchitectureDiagram />
 
292
  </div>
293
  )}
294
 
 
132
  )
133
  }
134
 
135
+ // ---------------------------------------------------------------------------
136
+ // Scaling Cost Panel
137
+ // ---------------------------------------------------------------------------
138
+ // Shows how LLM cost scales (or doesn't) with worker population. The design
139
+ // point: per-user cost is decoupled from LLM cost because all AI work happens
140
+ // at the zone and trigger-event level, not the worker level.
141
+
142
+ function ScalingCostPanel() {
143
+ const TIERS = [
144
+ {
145
+ label: 'Current (live)',
146
+ scale: '15 zones · template explanations',
147
+ cost: '~$0.05 / week',
148
+ note: 'Rule-based alert messages. Zero LLM spend on explanations.',
149
+ color: '#2a9d8f',
150
+ },
151
+ {
152
+ label: 'Pilot (106K workers)',
153
+ scale: '15 zones · AI explanations on',
154
+ cost: '~$0.10 / week',
155
+ note: 'Claude Haiku writes bilingual alerts for every trigger event. Cost scales with events fired, not workers.',
156
+ color: '#1565C0',
157
+ },
158
+ {
159
+ label: 'State-wide (10× coverage)',
160
+ scale: '150 zones · AI explanations on',
161
+ cost: '< $1 / week',
162
+ note: 'Because alerts are per trigger event (not per worker), population scale barely moves the cost line.',
163
+ color: '#d4a019',
164
+ },
165
+ ]
166
+
167
+ return (
168
+ <div style={{ marginTop: '32px', paddingLeft: '20px' }}>
169
+ <div style={{
170
+ fontSize: '0.72rem', fontWeight: 600, color: '#888',
171
+ textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '12px',
172
+ }}>
173
+ What it costs to run at scale
174
+ </div>
175
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: '12px' }}>
176
+ {TIERS.map(tier => (
177
+ <div key={tier.label} style={{
178
+ background: '#fff', border: '1px solid #e0dcd5', borderRadius: '8px',
179
+ padding: '16px 18px', borderLeft: `3px solid ${tier.color}`,
180
+ }}>
181
+ <div style={{ fontSize: '0.72rem', fontWeight: 600, color: tier.color, textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '6px' }}>
182
+ {tier.label}
183
+ </div>
184
+ <div style={{ fontSize: '1.35rem', fontWeight: 700, color: '#1a1a1a', fontFamily: 'Source Serif 4, serif', marginBottom: '4px' }}>
185
+ {tier.cost}
186
+ </div>
187
+ <div style={{ fontSize: '0.78rem', color: '#666', marginBottom: '8px' }}>
188
+ {tier.scale}
189
+ </div>
190
+ <div style={{ fontSize: '0.72rem', color: '#888', lineHeight: 1.5 }}>
191
+ {tier.note}
192
+ </div>
193
+ </div>
194
+ ))}
195
+ </div>
196
+ <p style={{ fontSize: '0.72rem', color: '#888', marginTop: '12px', fontStyle: 'italic' }}>
197
+ AI explanations are off by default. Set{' '}
198
+ <code style={{ background: '#f0ede8', padding: '1px 6px', borderRadius: '3px', fontSize: '0.7rem' }}>EXPLANATION_MODE=ai</code>
199
+ {' '}to enable. The capability is wired end-to-end either way.
200
+ </p>
201
+ </div>
202
+ )
203
+ }
204
 
205
  // ---------------------------------------------------------------------------
206
  // Main Component
 
357
  {activeTab === 'architecture' && (
358
  <div className="animate-tab-enter">
359
  <ArchitectureDiagram />
360
+ <ScalingCostPanel />
361
  </div>
362
  )}
363
 
src/explanation/explainer.py CHANGED
@@ -68,7 +68,7 @@ class ExplanationResult:
68
  class TriggerExplainer:
69
  """Generates bilingual heat alert explanations using Claude with RAG."""
70
 
71
- def __init__(self, api_key: Optional[str] = None, model: str = "claude-sonnet-4-20250514"):
72
  self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
73
  self.model = model
74
  self._client = None
 
68
  class TriggerExplainer:
69
  """Generates bilingual heat alert explanations using Claude with RAG."""
70
 
71
+ def __init__(self, api_key: Optional[str] = None, model: str = "claude-haiku-4-5-20251001"):
72
  self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
73
  self.model = model
74
  self._client = None
src/pipeline.py CHANGED
@@ -8,13 +8,14 @@ Each step has independent fallbacks — no cascading failures.
8
 
9
  import asyncio
10
  import logging
 
11
  import time
12
  import uuid
13
  from dataclasses import dataclass, field
14
  from datetime import datetime, timedelta
15
  from pathlib import Path
16
 
17
- from config import ZONES, ZONE_MAP
18
 
19
  # Pipeline runs only on Dar es Salaam zones (where the neural model is trained).
20
  # Other cities remain in config.py for the dashboard but don't run through the pipeline.
@@ -742,7 +743,17 @@ class HeatRiskPipeline:
742
  )
743
 
744
  try:
745
- explainer = TemplateExplainer() # Always use templates (free, instant)
 
 
 
 
 
 
 
 
 
 
746
 
747
  for trigger in self._triggers:
748
  zone = ZONE_MAP.get(trigger.zone_id)
@@ -750,12 +761,12 @@ class HeatRiskPipeline:
750
  continue
751
 
752
  basis = self._basis_risk.get(trigger.zone_id, {})
753
- # TemplateExplainer.explain is async — run it
754
  explanation = asyncio.run(explainer.explain(trigger, zone, basis))
755
  self._explanations.append(explanation)
756
  total_tokens += getattr(explanation, "tokens_used", 0)
757
 
758
- est_cost = total_tokens * 0.005 / 1000
 
759
 
760
  return StepResult(
761
  step="explain", status="ok",
@@ -764,6 +775,7 @@ class HeatRiskPipeline:
764
  details={
765
  "explanations_generated": len(self._explanations),
766
  "languages": ["en", "sw"],
 
767
  "total_tokens": total_tokens,
768
  "cost_usd": est_cost,
769
  },
 
8
 
9
  import asyncio
10
  import logging
11
+ import os
12
  import time
13
  import uuid
14
  from dataclasses import dataclass, field
15
  from datetime import datetime, timedelta
16
  from pathlib import Path
17
 
18
+ from config import ZONES, ZONE_MAP, EXPLANATION_MODE
19
 
20
  # Pipeline runs only on Dar es Salaam zones (where the neural model is trained).
21
  # Other cities remain in config.py for the dashboard but don't run through the pipeline.
 
743
  )
744
 
745
  try:
746
+ # Dispatch on EXPLANATION_MODE config flag.
747
+ # "template" — free, deterministic (default)
748
+ # "ai" — Claude Haiku 4.5 per trigger event. Scales with trigger
749
+ # events fired, not worker population — ~$0.10/wk even at
750
+ # 100K+ workers.
751
+ if EXPLANATION_MODE == "ai" and os.environ.get("ANTHROPIC_API_KEY"):
752
+ explainer = TriggerExplainer()
753
+ mode_used = "ai"
754
+ else:
755
+ explainer = TemplateExplainer()
756
+ mode_used = "template"
757
 
758
  for trigger in self._triggers:
759
  zone = ZONE_MAP.get(trigger.zone_id)
 
761
  continue
762
 
763
  basis = self._basis_risk.get(trigger.zone_id, {})
 
764
  explanation = asyncio.run(explainer.explain(trigger, zone, basis))
765
  self._explanations.append(explanation)
766
  total_tokens += getattr(explanation, "tokens_used", 0)
767
 
768
+ # Haiku 4.5 pricing: $1/M input, $5/M output. Rough blended estimate.
769
+ est_cost = total_tokens * 3 / 1_000_000 if mode_used == "ai" else 0.0
770
 
771
  return StepResult(
772
  step="explain", status="ok",
 
775
  details={
776
  "explanations_generated": len(self._explanations),
777
  "languages": ["en", "sw"],
778
+ "mode": mode_used,
779
  "total_tokens": total_tokens,
780
  "cost_usd": est_cost,
781
  },