Aperture / app /advisor.py
KSvend
fix: fix remaining indicator_id/indicator_ids references after rename
749b346
"""AOI Advisor — Claude-powered region insight for analysis recommendations."""
from __future__ import annotations
import json
import logging
from datetime import date
from app.config import ANTHROPIC_API_KEY
logger = logging.getLogger(__name__)
_SYSTEM_PROMPT = """\
You are a remote sensing advisor for humanitarian programme teams. Given a geographic location, provide a brief analysis recommendation.
Available EO products:
- ndvi: Vegetation health from Sentinel-2. Detects drought, crop stress, deforestation.
- water: Water extent (MNDWI) from Sentinel-2. Detects flooding, drought, reservoir changes.
- sar: Radar backscatter from Sentinel-1. Detects ground surface changes, flooding, construction.
- buildup: Settlement extent (NDBI) from Sentinel-2. Detects urban growth, displacement camps.
Coverage: East Africa region. Resolution: 100m. Max analysis window: 3 years.
Respond with JSON only, no markdown. Structure:
{
"context": "1-3 sentences about this region and recent relevant events",
"recommended_start": "YYYY-MM-DD",
"recommended_end": "YYYY-MM-DD",
"product_priorities": ["product_id", ...],
"reasoning": "1 sentence per EO product explaining why it is relevant here"
}"""
_EMPTY_RESPONSE = {
"context": None,
"recommended_start": None,
"recommended_end": None,
"product_priorities": None,
"reasoning": None,
}
# Lazily-initialized Anthropic client (reused across requests for connection pooling)
_client = None
def _get_client():
global _client
if _client is None:
import anthropic
_client = anthropic.AsyncAnthropic(api_key=ANTHROPIC_API_KEY)
return _client
async def get_aoi_advice(bbox: list[float]) -> dict:
"""Call Claude to get region-aware analysis recommendations.
Returns structured advice dict. On any failure, returns all-null dict.
"""
if not ANTHROPIC_API_KEY:
logger.warning("ANTHROPIC_API_KEY not set — skipping AOI advisor")
return _EMPTY_RESPONSE
center_lng = (bbox[0] + bbox[2]) / 2
center_lat = (bbox[1] + bbox[3]) / 2
today = date.today().isoformat()
lat_label = f"{abs(center_lat):.4f}°{'N' if center_lat >= 0 else 'S'}"
lng_label = f"{abs(center_lng):.4f}°{'E' if center_lng >= 0 else 'W'}"
user_prompt = (
f"Location: {lat_label}, {lng_label}\n"
f"Current date: {today}\n"
f"Recommend an analysis timeframe and indicator priorities for this area."
)
try:
client = _get_client()
message = await client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=500,
temperature=0,
system=_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_prompt}],
)
raw = message.content[0].text
advice = json.loads(raw)
# Validate expected keys are present
for key in ("context", "recommended_start", "recommended_end", "product_priorities", "reasoning"):
if key not in advice:
logger.warning("AOI advisor response missing key: %s", key)
return _EMPTY_RESPONSE
# Validate dates are parseable
date.fromisoformat(advice["recommended_start"])
date.fromisoformat(advice["recommended_end"])
# Filter product_priorities to only known indicators
valid_ids = {"ndvi", "water", "sar", "buildup"}
advice["product_priorities"] = [
i for i in advice["product_priorities"] if i in valid_ids
]
return advice
except Exception as exc:
logger.warning("AOI advisor failed: %s", exc)
return _EMPTY_RESPONSE