KSvend Claude Happy commited on
Commit
b595465
·
1 Parent(s): 12eb573

docs: implementation plan for AOI advisor (Claude-powered region insight)

Browse files

8 tasks: dependency, config/model, advisor module, API endpoint,
frontend API wrapper, HTML/CSS card, app.js wiring, startup logging.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

docs/superpowers/plans/2026-04-06-aoi-advisor.md ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AOI Advisor Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a Claude-powered advisor that recommends analysis timeframes and indicator priorities when a user places an AOI.
6
+
7
+ **Architecture:** Frontend calls `POST /api/aoi-advice` after AOI placement. Backend endpoint calls Claude Haiku with coordinates and available indicators. Response is structured JSON that populates an advice card and auto-fills date pickers. Graceful degradation if API key missing or call fails.
8
+
9
+ **Tech Stack:** Anthropic Python SDK (`anthropic`), FastAPI endpoint, vanilla JS frontend.
10
+
11
+ **Prerequisite:** The AOI click-to-place plan must be implemented first (the advisor wires into the `onAoiChange` callback from that plan).
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ | File | Role | Action |
18
+ |------|------|--------|
19
+ | `pyproject.toml` | Dependencies | Add `anthropic` |
20
+ | `app/config.py` | Config | Add `ANTHROPIC_API_KEY` |
21
+ | `app/models.py` | Request model | Add `AoiAdviceRequest` |
22
+ | `app/advisor.py` | Claude API call | Create: prompt builder + API call |
23
+ | `app/main.py` | Route registration | Add `/api/aoi-advice` endpoint |
24
+ | `frontend/js/api.js` | API wrapper | Add `getAoiAdvice()` |
25
+ | `frontend/index.html` | Advisor card HTML | Add card between size toggles and dates |
26
+ | `frontend/css/merlx.css` | Card styles | Add `.aoi-advisor` styles |
27
+ | `frontend/js/app.js` | Wire advisor | Call API on AOI change, populate card, auto-fill dates |
28
+
29
+ ---
30
+
31
+ ### Task 1: Add anthropic dependency
32
+
33
+ **Files:**
34
+ - Modify: `pyproject.toml:6-27`
35
+
36
+ - [ ] **Step 1: Add anthropic to dependencies**
37
+
38
+ In `pyproject.toml`, add `"anthropic>=0.40.0",` to the dependencies list, after the `"openeo>=0.28.0",` line:
39
+
40
+ ```toml
41
+ "openeo>=0.28.0",
42
+ "anthropic>=0.40.0",
43
+ ]
44
+ ```
45
+
46
+ - [ ] **Step 2: Install the dependency**
47
+
48
+ ```bash
49
+ pip install anthropic>=0.40.0
50
+ ```
51
+
52
+ - [ ] **Step 3: Commit**
53
+
54
+ ```bash
55
+ git add pyproject.toml
56
+ git commit -m "chore: add anthropic SDK dependency"
57
+ ```
58
+
59
+ ---
60
+
61
+ ### Task 2: Add config and request model
62
+
63
+ **Files:**
64
+ - Modify: `app/config.py:20-21`
65
+ - Modify: `app/models.py:149` (end of file)
66
+
67
+ - [ ] **Step 1: Add ANTHROPIC_API_KEY to config**
68
+
69
+ In `app/config.py`, add after the `OPENEO_CLIENT_SECRET` line (line 20):
70
+
71
+ ```python
72
+ # Anthropic API key for AOI advisor (Claude-powered region insight).
73
+ ANTHROPIC_API_KEY: str | None = os.environ.get("ANTHROPIC_API_KEY")
74
+ ```
75
+
76
+ - [ ] **Step 2: Add AoiAdviceRequest model**
77
+
78
+ In `app/models.py`, add at the end of the file (after `IndicatorMeta`):
79
+
80
+ ```python
81
+ class AoiAdviceRequest(BaseModel):
82
+ bbox: list[float] = Field(min_length=4, max_length=4)
83
+ ```
84
+
85
+ - [ ] **Step 3: Commit**
86
+
87
+ ```bash
88
+ git add app/config.py app/models.py
89
+ git commit -m "feat: add ANTHROPIC_API_KEY config and AoiAdviceRequest model"
90
+ ```
91
+
92
+ ---
93
+
94
+ ### Task 3: Create advisor module
95
+
96
+ **Files:**
97
+ - Create: `app/advisor.py`
98
+
99
+ - [ ] **Step 1: Create app/advisor.py**
100
+
101
+ ```python
102
+ """AOI Advisor — Claude-powered region insight for analysis recommendations."""
103
+ from __future__ import annotations
104
+
105
+ import json
106
+ import logging
107
+ from datetime import date
108
+
109
+ from app.config import ANTHROPIC_API_KEY
110
+
111
+ logger = logging.getLogger(__name__)
112
+
113
+ _SYSTEM_PROMPT = """\
114
+ You are a remote sensing advisor for humanitarian programme teams. Given a geographic location, provide a brief analysis recommendation.
115
+
116
+ Available Earth observation indicators:
117
+ - ndvi: Vegetation health from Sentinel-2. Detects drought, crop stress, deforestation.
118
+ - water: Water extent (MNDWI) from Sentinel-2. Detects flooding, drought, reservoir changes.
119
+ - sar: Radar backscatter from Sentinel-1. Detects ground surface changes, flooding, construction.
120
+ - buildup: Settlement extent (NDBI) from Sentinel-2. Detects urban growth, displacement camps.
121
+
122
+ Coverage: East Africa region. Resolution: 100m. Max analysis window: 3 years.
123
+
124
+ Respond with JSON only, no markdown. Structure:
125
+ {
126
+ "context": "1-3 sentences about this region and recent relevant events",
127
+ "recommended_start": "YYYY-MM-DD",
128
+ "recommended_end": "YYYY-MM-DD",
129
+ "indicator_priorities": ["indicator_id", ...],
130
+ "reasoning": "1 sentence per indicator explaining why it is relevant here"
131
+ }"""
132
+
133
+ _EMPTY_RESPONSE = {
134
+ "context": None,
135
+ "recommended_start": None,
136
+ "recommended_end": None,
137
+ "indicator_priorities": None,
138
+ "reasoning": None,
139
+ }
140
+
141
+
142
+ async def get_aoi_advice(bbox: list[float]) -> dict:
143
+ """Call Claude to get region-aware analysis recommendations.
144
+
145
+ Returns structured advice dict. On any failure, returns all-null dict.
146
+ """
147
+ if not ANTHROPIC_API_KEY:
148
+ logger.warning("ANTHROPIC_API_KEY not set — skipping AOI advisor")
149
+ return _EMPTY_RESPONSE
150
+
151
+ center_lng = (bbox[0] + bbox[2]) / 2
152
+ center_lat = (bbox[1] + bbox[3]) / 2
153
+ today = date.today().isoformat()
154
+
155
+ user_prompt = (
156
+ f"Location: {center_lat:.4f}°N, {center_lng:.4f}°E\n"
157
+ f"Current date: {today}\n"
158
+ f"Recommend an analysis timeframe and indicator priorities for this area."
159
+ )
160
+
161
+ try:
162
+ import anthropic
163
+
164
+ client = anthropic.AsyncAnthropic(api_key=ANTHROPIC_API_KEY)
165
+ message = await client.messages.create(
166
+ model="claude-haiku-4-5-20251001",
167
+ max_tokens=500,
168
+ temperature=0,
169
+ system=_SYSTEM_PROMPT,
170
+ messages=[{"role": "user", "content": user_prompt}],
171
+ )
172
+
173
+ raw = message.content[0].text
174
+ advice = json.loads(raw)
175
+
176
+ # Validate expected keys are present
177
+ for key in ("context", "recommended_start", "recommended_end", "indicator_priorities", "reasoning"):
178
+ if key not in advice:
179
+ logger.warning("AOI advisor response missing key: %s", key)
180
+ return _EMPTY_RESPONSE
181
+
182
+ # Validate dates are parseable
183
+ date.fromisoformat(advice["recommended_start"])
184
+ date.fromisoformat(advice["recommended_end"])
185
+
186
+ # Filter indicator_priorities to only known indicators
187
+ valid_ids = {"ndvi", "water", "sar", "buildup"}
188
+ advice["indicator_priorities"] = [
189
+ i for i in advice["indicator_priorities"] if i in valid_ids
190
+ ]
191
+
192
+ return advice
193
+
194
+ except Exception as exc:
195
+ logger.warning("AOI advisor failed: %s", exc)
196
+ return _EMPTY_RESPONSE
197
+ ```
198
+
199
+ - [ ] **Step 2: Verify it imports cleanly**
200
+
201
+ ```bash
202
+ cd /Users/kmini/GitHub/Aperture && python3 -c "from app.advisor import get_aoi_advice; print('OK')"
203
+ ```
204
+
205
+ Expected: `OK`
206
+
207
+ - [ ] **Step 3: Commit**
208
+
209
+ ```bash
210
+ git add app/advisor.py
211
+ git commit -m "feat: add advisor module — Claude-powered region insight"
212
+ ```
213
+
214
+ ---
215
+
216
+ ### Task 4: Add API endpoint
217
+
218
+ **Files:**
219
+ - Modify: `app/main.py:15-16` (imports)
220
+ - Modify: `app/main.py:57` (after router includes, before health endpoint)
221
+
222
+ - [ ] **Step 1: Add imports**
223
+
224
+ In `app/main.py`, add to the existing imports (after line 15):
225
+
226
+ ```python
227
+ from app.advisor import get_aoi_advice
228
+ from app.models import AoiAdviceRequest
229
+ ```
230
+
231
+ - [ ] **Step 2: Add the endpoint**
232
+
233
+ In `app/main.py`, add after the `app.include_router(auth_router)` line (line 57) and before the `@app.get("/health")` line:
234
+
235
+ ```python
236
+ @app.post("/api/aoi-advice")
237
+ async def aoi_advice(request: AoiAdviceRequest, email: str = Depends(get_current_user)):
238
+ return await get_aoi_advice(request.bbox)
239
+ ```
240
+
241
+ - [ ] **Step 3: Verify the server starts**
242
+
243
+ ```bash
244
+ cd /Users/kmini/GitHub/Aperture && python3 -c "from app.main import create_app; app = create_app(); print('OK')"
245
+ ```
246
+
247
+ Expected: `OK`
248
+
249
+ - [ ] **Step 4: Commit**
250
+
251
+ ```bash
252
+ git add app/main.py
253
+ git commit -m "feat: add /api/aoi-advice endpoint"
254
+ ```
255
+
256
+ ---
257
+
258
+ ### Task 5: Add frontend API function
259
+
260
+ **Files:**
261
+ - Modify: `frontend/js/api.js:134` (after `spatialUrl` function)
262
+
263
+ - [ ] **Step 1: Add getAoiAdvice function**
264
+
265
+ In `frontend/js/api.js`, add before the `/* ── Auth */` section (before line 136):
266
+
267
+ ```javascript
268
+ /* ── AOI Advisor ───────────────────────────────────────── */
269
+
270
+ /**
271
+ * Get Claude-powered region insight for an AOI.
272
+ * @param {Array<number>} bbox - [minLon, minLat, maxLon, maxLat]
273
+ * @returns {Promise<{context, recommended_start, recommended_end, indicator_priorities, reasoning} | null>}
274
+ */
275
+ export async function getAoiAdvice(bbox) {
276
+ try {
277
+ const result = await apiFetch('/api/aoi-advice', {
278
+ method: 'POST',
279
+ body: JSON.stringify({ bbox }),
280
+ });
281
+ // Treat all-null as failure
282
+ if (!result || !result.context) return null;
283
+ return result;
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+ ```
289
+
290
+ - [ ] **Step 2: Commit**
291
+
292
+ ```bash
293
+ git add frontend/js/api.js
294
+ git commit -m "feat: add getAoiAdvice API wrapper"
295
+ ```
296
+
297
+ ---
298
+
299
+ ### Task 6: Add advisor card HTML and CSS
300
+
301
+ **Files:**
302
+ - Modify: `frontend/index.html` (after the size toggles `aoi-area-display` div, before the `<hr class="divider" />`)
303
+ - Modify: `frontend/css/merlx.css` (after `.aoi-area-display` styles)
304
+
305
+ - [ ] **Step 1: Add advisor card HTML**
306
+
307
+ In `frontend/index.html`, add after the `<div id="aoi-area-display"...></div>` line and before the `<hr class="divider" />` line:
308
+
309
+ ```html
310
+ <!-- AOI Advisor -->
311
+ <div id="aoi-advisor" class="aoi-advisor" style="display:none;">
312
+ <div id="aoi-advisor-loading" class="aoi-advisor-loading">Analyzing region…</div>
313
+ <div id="aoi-advisor-content" style="display:none;">
314
+ <div class="aoi-advisor-heading">Region Insight</div>
315
+ <p id="advisor-context" class="aoi-advisor-text"></p>
316
+ <p id="advisor-dates" class="aoi-advisor-meta"></p>
317
+ <p id="advisor-reasoning" class="aoi-advisor-text" style="font-style: italic;"></p>
318
+ <div id="advisor-priorities" class="aoi-advisor-priorities"></div>
319
+ </div>
320
+ </div>
321
+ ```
322
+
323
+ - [ ] **Step 2: Add advisor CSS**
324
+
325
+ In `frontend/css/merlx.css`, add after the `.aoi-area-display` block:
326
+
327
+ ```css
328
+ /* AOI Advisor card */
329
+ .aoi-advisor {
330
+ background: var(--shell-warm);
331
+ border: 1px solid var(--border);
332
+ border-radius: var(--radius-sm);
333
+ padding: var(--space-5);
334
+ margin-top: var(--space-3);
335
+ }
336
+
337
+ .aoi-advisor-loading {
338
+ font-family: var(--font-ui);
339
+ font-size: var(--text-xs);
340
+ color: var(--ink-muted);
341
+ animation: pulse 1.5s ease-in-out infinite;
342
+ }
343
+
344
+ @keyframes pulse {
345
+ 0%, 100% { opacity: 1; }
346
+ 50% { opacity: 0.4; }
347
+ }
348
+
349
+ .aoi-advisor-heading {
350
+ font-family: var(--font-ui);
351
+ font-size: var(--text-sm);
352
+ font-weight: 600;
353
+ color: var(--ink);
354
+ margin-bottom: var(--space-2);
355
+ }
356
+
357
+ .aoi-advisor-text {
358
+ font-family: var(--font-ui);
359
+ font-size: var(--text-xs);
360
+ color: var(--ink);
361
+ line-height: 1.5;
362
+ margin: 0 0 var(--space-3) 0;
363
+ }
364
+
365
+ .aoi-advisor-meta {
366
+ font-family: var(--font-data);
367
+ font-size: var(--text-xs);
368
+ color: var(--ink-muted);
369
+ margin: 0 0 var(--space-2) 0;
370
+ }
371
+
372
+ .aoi-advisor-priorities {
373
+ display: flex;
374
+ gap: var(--space-2);
375
+ flex-wrap: wrap;
376
+ }
377
+
378
+ .aoi-advisor-pill {
379
+ font-family: var(--font-data);
380
+ font-size: 10px;
381
+ padding: 2px 8px;
382
+ border-radius: 10px;
383
+ background: var(--iris-dim);
384
+ color: var(--iris-dark);
385
+ border: 1px solid var(--iris);
386
+ }
387
+ ```
388
+
389
+ - [ ] **Step 3: Commit**
390
+
391
+ ```bash
392
+ git add frontend/index.html frontend/css/merlx.css
393
+ git commit -m "feat: add advisor card HTML and CSS styles"
394
+ ```
395
+
396
+ ---
397
+
398
+ ### Task 7: Wire advisor into app.js
399
+
400
+ **Files:**
401
+ - Modify: `frontend/js/app.js:7` (imports)
402
+ - Modify: `frontend/js/app.js` (inside `setupDefineArea()` function)
403
+
404
+ This task assumes the click-to-place plan has been implemented, so `setupDefineArea()` uses the rewritten version from that plan.
405
+
406
+ - [ ] **Step 1: Update imports**
407
+
408
+ In `frontend/js/app.js`, update the api.js import line to add `getAoiAdvice`:
409
+
410
+ Change:
411
+
412
+ ```javascript
413
+ import { submitJob, getJob, requestMagicLink, verifyToken, listJobs } from './api.js';
414
+ ```
415
+
416
+ to:
417
+
418
+ ```javascript
419
+ import { submitJob, getJob, requestMagicLink, verifyToken, listJobs, getAoiAdvice } from './api.js';
420
+ ```
421
+
422
+ - [ ] **Step 2: Add advisor wiring inside setupDefineArea**
423
+
424
+ In `frontend/js/app.js`, inside the `setupDefineArea()` function, find the `initAoiMap('map', (bbox) => {` callback. Replace the entire callback with:
425
+
426
+ ```javascript
427
+ initAoiMap('map', async (bbox) => {
428
+ _currentBbox = bbox;
429
+ if (bbox) {
430
+ const area = _bboxAreaKm2(bbox);
431
+ areaDisplay.style.display = '';
432
+ areaDisplay.textContent = `${Math.round(area).toLocaleString()} km²`;
433
+ areaDisplay.className = 'aoi-area-display';
434
+
435
+ // Show advisor loading, disable Continue
436
+ const advisorEl = document.getElementById('aoi-advisor');
437
+ const advisorLoading = document.getElementById('aoi-advisor-loading');
438
+ const advisorContent = document.getElementById('aoi-advisor-content');
439
+ advisorEl.style.display = '';
440
+ advisorLoading.style.display = '';
441
+ advisorContent.style.display = 'none';
442
+ continueBtn.disabled = true;
443
+
444
+ // Fetch advice
445
+ const advice = await getAoiAdvice(bbox);
446
+
447
+ if (advice) {
448
+ // Populate card
449
+ document.getElementById('advisor-context').textContent = advice.context;
450
+ document.getElementById('advisor-dates').textContent =
451
+ `Recommended: ${advice.recommended_start} to ${advice.recommended_end}`;
452
+ document.getElementById('advisor-reasoning').textContent = advice.reasoning;
453
+
454
+ // Indicator priority pills
455
+ const pillsEl = document.getElementById('advisor-priorities');
456
+ pillsEl.innerHTML = '';
457
+ const names = { ndvi: 'NDVI', water: 'Water', sar: 'SAR', buildup: 'Built-up' };
458
+ for (const id of (advice.indicator_priorities || [])) {
459
+ const pill = document.createElement('span');
460
+ pill.className = 'aoi-advisor-pill';
461
+ pill.textContent = names[id] || id;
462
+ pillsEl.appendChild(pill);
463
+ }
464
+
465
+ // Auto-fill date pickers
466
+ document.getElementById('date-start').value = advice.recommended_start;
467
+ document.getElementById('date-end').value = advice.recommended_end;
468
+
469
+ // Show content, hide loading
470
+ advisorLoading.style.display = 'none';
471
+ advisorContent.style.display = '';
472
+ } else {
473
+ // No advice — hide card silently
474
+ advisorEl.style.display = 'none';
475
+ }
476
+
477
+ continueBtn.disabled = false;
478
+ } else {
479
+ areaDisplay.style.display = 'none';
480
+ document.getElementById('aoi-advisor').style.display = 'none';
481
+ continueBtn.disabled = true;
482
+ }
483
+ });
484
+ ```
485
+
486
+ Note: the callback is now `async` — this is fine because MapLibre click callbacks don't use the return value.
487
+
488
+ - [ ] **Step 3: Verify the full flow**
489
+
490
+ 1. Open the app, log in, navigate to Define Area
491
+ 2. Click on the map → AOI appears, "Analyzing region..." shows
492
+ 3. After 1-3 seconds, advice card populates with context, dates, priorities
493
+ 4. Date pickers are auto-filled with recommended dates
494
+ 5. Continue button enables after advice loads
495
+ 6. If `ANTHROPIC_API_KEY` is not set: advice card hides silently, Continue enables immediately
496
+
497
+ - [ ] **Step 4: Commit**
498
+
499
+ ```bash
500
+ git add frontend/js/app.js
501
+ git commit -m "feat: wire AOI advisor into Define Area page"
502
+ ```
503
+
504
+ ---
505
+
506
+ ### Task 8: Add startup log for API key status
507
+
508
+ **Files:**
509
+ - Modify: `app/main.py:28-32` (lifespan startup logging)
510
+
511
+ - [ ] **Step 1: Add Anthropic key status to startup log**
512
+
513
+ In `app/main.py`, inside the `lifespan` function, after the CDSE credential check (after line 32), add:
514
+
515
+ ```python
516
+ from app.config import ANTHROPIC_API_KEY
517
+ if ANTHROPIC_API_KEY:
518
+ print(f"[Aperture] Anthropic API key configured (AOI advisor enabled)")
519
+ else:
520
+ print("[Aperture] Anthropic API key not set — AOI advisor disabled")
521
+ ```
522
+
523
+ - [ ] **Step 2: Commit**
524
+
525
+ ```bash
526
+ git add app/main.py
527
+ git commit -m "chore: log Anthropic API key status on startup"
528
+ ```