KSvend commited on
Commit ·
9f90242
1
Parent(s): 7534962
feat: add advisor module — Claude-powered region insight
Browse files- app/advisor.py +95 -0
app/advisor.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AOI Advisor — Claude-powered region insight for analysis recommendations."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
from datetime import date
|
| 7 |
+
|
| 8 |
+
from app.config import ANTHROPIC_API_KEY
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
_SYSTEM_PROMPT = """\
|
| 13 |
+
You are a remote sensing advisor for humanitarian programme teams. Given a geographic location, provide a brief analysis recommendation.
|
| 14 |
+
|
| 15 |
+
Available Earth observation indicators:
|
| 16 |
+
- ndvi: Vegetation health from Sentinel-2. Detects drought, crop stress, deforestation.
|
| 17 |
+
- water: Water extent (MNDWI) from Sentinel-2. Detects flooding, drought, reservoir changes.
|
| 18 |
+
- sar: Radar backscatter from Sentinel-1. Detects ground surface changes, flooding, construction.
|
| 19 |
+
- buildup: Settlement extent (NDBI) from Sentinel-2. Detects urban growth, displacement camps.
|
| 20 |
+
|
| 21 |
+
Coverage: East Africa region. Resolution: 100m. Max analysis window: 3 years.
|
| 22 |
+
|
| 23 |
+
Respond with JSON only, no markdown. Structure:
|
| 24 |
+
{
|
| 25 |
+
"context": "1-3 sentences about this region and recent relevant events",
|
| 26 |
+
"recommended_start": "YYYY-MM-DD",
|
| 27 |
+
"recommended_end": "YYYY-MM-DD",
|
| 28 |
+
"indicator_priorities": ["indicator_id", ...],
|
| 29 |
+
"reasoning": "1 sentence per indicator explaining why it is relevant here"
|
| 30 |
+
}"""
|
| 31 |
+
|
| 32 |
+
_EMPTY_RESPONSE = {
|
| 33 |
+
"context": None,
|
| 34 |
+
"recommended_start": None,
|
| 35 |
+
"recommended_end": None,
|
| 36 |
+
"indicator_priorities": None,
|
| 37 |
+
"reasoning": None,
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def get_aoi_advice(bbox: list[float]) -> dict:
|
| 42 |
+
"""Call Claude to get region-aware analysis recommendations.
|
| 43 |
+
|
| 44 |
+
Returns structured advice dict. On any failure, returns all-null dict.
|
| 45 |
+
"""
|
| 46 |
+
if not ANTHROPIC_API_KEY:
|
| 47 |
+
logger.warning("ANTHROPIC_API_KEY not set — skipping AOI advisor")
|
| 48 |
+
return _EMPTY_RESPONSE
|
| 49 |
+
|
| 50 |
+
center_lng = (bbox[0] + bbox[2]) / 2
|
| 51 |
+
center_lat = (bbox[1] + bbox[3]) / 2
|
| 52 |
+
today = date.today().isoformat()
|
| 53 |
+
|
| 54 |
+
user_prompt = (
|
| 55 |
+
f"Location: {center_lat:.4f}°N, {center_lng:.4f}°E\n"
|
| 56 |
+
f"Current date: {today}\n"
|
| 57 |
+
f"Recommend an analysis timeframe and indicator priorities for this area."
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
import anthropic
|
| 62 |
+
|
| 63 |
+
client = anthropic.AsyncAnthropic(api_key=ANTHROPIC_API_KEY)
|
| 64 |
+
message = await client.messages.create(
|
| 65 |
+
model="claude-haiku-4-5-20251001",
|
| 66 |
+
max_tokens=500,
|
| 67 |
+
temperature=0,
|
| 68 |
+
system=_SYSTEM_PROMPT,
|
| 69 |
+
messages=[{"role": "user", "content": user_prompt}],
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
raw = message.content[0].text
|
| 73 |
+
advice = json.loads(raw)
|
| 74 |
+
|
| 75 |
+
# Validate expected keys are present
|
| 76 |
+
for key in ("context", "recommended_start", "recommended_end", "indicator_priorities", "reasoning"):
|
| 77 |
+
if key not in advice:
|
| 78 |
+
logger.warning("AOI advisor response missing key: %s", key)
|
| 79 |
+
return _EMPTY_RESPONSE
|
| 80 |
+
|
| 81 |
+
# Validate dates are parseable
|
| 82 |
+
date.fromisoformat(advice["recommended_start"])
|
| 83 |
+
date.fromisoformat(advice["recommended_end"])
|
| 84 |
+
|
| 85 |
+
# Filter indicator_priorities to only known indicators
|
| 86 |
+
valid_ids = {"ndvi", "water", "sar", "buildup"}
|
| 87 |
+
advice["indicator_priorities"] = [
|
| 88 |
+
i for i in advice["indicator_priorities"] if i in valid_ids
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
return advice
|
| 92 |
+
|
| 93 |
+
except Exception as exc:
|
| 94 |
+
logger.warning("AOI advisor failed: %s", exc)
|
| 95 |
+
return _EMPTY_RESPONSE
|