Upload app_risk_report.py
Browse files- app_risk_report.py +256 -30
app_risk_report.py
CHANGED
|
@@ -1475,7 +1475,7 @@ def _build_benchmark_md(forecast_model: str, forecast_data: dict,
|
|
| 1475 |
# Architecture:
|
| 1476 |
# - Python detects trigger conditions deterministically (forecast RMSE + Winkler ratio,
|
| 1477 |
# satellite scene count) and ranks forecast alternatives from diagnostics.
|
| 1478 |
-
# - Historical non-stationarity is retained as evidence
|
| 1479 |
# - Satellite retries preserve the same variable and widen the evidence window/cloud threshold.
|
| 1480 |
# - GPT is used only for the forecast fallback judgment and final narrative.
|
| 1481 |
# - Hard caps: MAX_LOOPS_PER_AGENT=2 (satellite), REACT_FORECAST_MAX_RUNS=3, GLOBAL_LOOP_CAP=8.
|
|
@@ -2263,36 +2263,88 @@ def _satellite_full_for_report(location, variable, days, max_cloud, spectral_ind
|
|
| 2263 |
return satellite_data, sat_outputs
|
| 2264 |
|
| 2265 |
|
| 2266 |
-
def
|
| 2267 |
-
"""
|
| 2268 |
-
|
| 2269 |
-
|
| 2270 |
-
|
| 2271 |
-
|
| 2272 |
-
|
| 2273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2274 |
|
| 2275 |
-
wx_outputs = analyze_weather_ui(location, variable, current_days)
|
| 2276 |
-
weather_data = _weather_data_from_ui_outputs(wx_outputs)
|
| 2277 |
|
| 2278 |
-
|
| 2279 |
-
|
| 2280 |
-
|
| 2281 |
-
|
| 2282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2283 |
if error:
|
| 2284 |
-
|
| 2285 |
-
observation = f"Historical evidence unavailable: {error}."
|
| 2286 |
-
elif _is_nonstationary(stat_conc):
|
| 2287 |
-
observation = (
|
| 2288 |
-
"Non-stationarity detected and retained as a genuine time-series diagnostic; "
|
| 2289 |
-
"no window escalation is triggered."
|
| 2290 |
-
)
|
| 2291 |
-
else:
|
| 2292 |
-
observation = "Historical evidence available; no retry required."
|
| 2293 |
|
| 2294 |
-
|
| 2295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2296 |
|
| 2297 |
|
| 2298 |
def _react_satellite_agent(location, sat_variable, days, max_cloud, openai_api_key, react_log):
|
|
@@ -2543,6 +2595,141 @@ def format_react_log(react_log: list) -> str:
|
|
| 2543 |
]
|
| 2544 |
return "\n".join(lines)
|
| 2545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2546 |
def generate_risk_report_ui(
|
| 2547 |
location, variable, forecast_model, days, satellite_days, max_cloud, sat_variable
|
| 2548 |
):
|
|
@@ -2777,6 +2964,31 @@ def generate_risk_report_ui(
|
|
| 2777 |
forecast_evidence = forecast_data.get("structured_evidence") or "Unavailable"
|
| 2778 |
consistency = satellite_data.get("metadata_spectral_consistency", [])
|
| 2779 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2780 |
synthesis_prompt = f"""
|
| 2781 |
You are a senior climate-risk analyst. Produce a concise, evidence-grounded report for **{location}**.
|
| 2782 |
|
|
@@ -2795,6 +3007,9 @@ Recent window: {final_days} days
|
|
| 2795 |
{historical_evidence}
|
| 2796 |
Diagnostic stationarity note: {weather_data.get('stationarity_conclusion', 'N/A')}
|
| 2797 |
Historical data error: {weather_data.get('error', 'none')}
|
|
|
|
|
|
|
|
|
|
| 2798 |
|
| 2799 |
## SATELLITE INDEX GLOSSARY — PLAIN LANGUAGE
|
| 2800 |
- NDVI (Normalized Difference Vegetation Index): vegetation greenness/health; higher generally means denser, more active vegetation.
|
|
@@ -2842,6 +3057,13 @@ Each row aligns the app forecast and Open-Meteo operational reference on the sam
|
|
| 2842 |
Use these rows exactly in the 7-Day Outlook; do not recalculate the differences.
|
| 2843 |
{json.dumps(daily_outlook_comparison, ensure_ascii=False)}
|
| 2844 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2845 |
## E) RECENT / UPCOMING EXTERNAL EVENT INTELLIGENCE
|
| 2846 |
{str(pplx_news_result.get('content') or 'External event intelligence unavailable.')[:1600]}
|
| 2847 |
|
|
@@ -2980,7 +3202,11 @@ For Data Quality, Uncertainty & Scope use exactly four bullets: Forecast reliabi
|
|
| 2980 |
if missing_report.strip():
|
| 2981 |
gpt_report = gpt_report.rstrip() + "\n\n---\n\n" + missing_report.strip()
|
| 2982 |
|
| 2983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2984 |
except Exception as exc:
|
| 2985 |
status_log[-1] = f"⚠️ [6/6] GPT error: {exc}"
|
| 2986 |
gpt_report = f"GPT synthesis failed: {exc}"
|
|
@@ -3013,7 +3239,7 @@ with gr.Blocks() as iface_report:
|
|
| 3013 |
gr.Markdown(
|
| 3014 |
"Orchestrates Historical, Sentinel-2 and Forecast agents, compares the statistical "
|
| 3015 |
"forecast with **Open-Meteo as an operational reference forecast**, and queries "
|
| 3016 |
-
"**Perplexity** for recent/upcoming extreme-event intelligence.
|
| 3017 |
"already-structured evidence; it does not create numeric risk scores.\n\n"
|
| 3018 |
"_The report is hazard-focused: exposure and vulnerability are not modelled. "
|
| 3019 |
"OpenAI and Perplexity credentials are loaded server-side from Hugging Face Space Secrets._"
|
|
@@ -3078,7 +3304,7 @@ with gr.Blocks() as iface_report:
|
|
| 3078 |
rp_forecast_summary = gr.State("")
|
| 3079 |
|
| 3080 |
with gr.Tabs():
|
| 3081 |
-
with gr.Tab("📋 Hazard-Focused Report (
|
| 3082 |
rp_gpt_report = gr.Markdown(label="Full Report")
|
| 3083 |
|
| 3084 |
with gr.Tab("📡 Operational Reference (Open-Meteo)"):
|
|
|
|
| 1475 |
# Architecture:
|
| 1476 |
# - Python detects trigger conditions deterministically (forecast RMSE + Winkler ratio,
|
| 1477 |
# satellite scene count) and ranks forecast alternatives from diagnostics.
|
| 1478 |
+
# - Historical non-stationarity is retained as evidence; missing historical outputs trigger bounded retries.
|
| 1479 |
# - Satellite retries preserve the same variable and widen the evidence window/cloud threshold.
|
| 1480 |
# - GPT is used only for the forecast fallback judgment and final narrative.
|
| 1481 |
# - Hard caps: MAX_LOOPS_PER_AGENT=2 (satellite), REACT_FORECAST_MAX_RUNS=3, GLOBAL_LOOP_CAP=8.
|
|
|
|
| 2263 |
return satellite_data, sat_outputs
|
| 2264 |
|
| 2265 |
|
| 2266 |
+
def _extract_weather_anomalies(evidence: str, variable: str) -> list[dict]:
|
| 2267 |
+
"""Surface explicit anomaly statements without inventing thresholds or measurements."""
|
| 2268 |
+
import re
|
| 2269 |
+
|
| 2270 |
+
signals = ("anomal", "unusual", "exceptional", "extreme", "record", "above normal",
|
| 2271 |
+
"below normal", "above average", "below average", "departure", "deviation")
|
| 2272 |
+
negations = ("no anomaly", "no anomalies", "no significant anomal", "without anomal",
|
| 2273 |
+
"not anomalous", "no unusual", "no extreme")
|
| 2274 |
+
findings = []
|
| 2275 |
+
seen = set()
|
| 2276 |
+
# Interpret each sentence/line once; never repeat a remote data request.
|
| 2277 |
+
for fragment in re.split(r"[\n]+|(?<=[.!?])\s+", str(evidence or "")):
|
| 2278 |
+
statement = fragment.strip().lstrip("-* ")
|
| 2279 |
+
lower = statement.lower()
|
| 2280 |
+
if not statement or not any(token in lower for token in signals):
|
| 2281 |
+
continue
|
| 2282 |
+
if any(negative in lower for negative in negations):
|
| 2283 |
+
continue
|
| 2284 |
+
key = lower.casefold()
|
| 2285 |
+
if key in seen:
|
| 2286 |
+
continue
|
| 2287 |
+
seen.add(key)
|
| 2288 |
+
findings.append({
|
| 2289 |
+
"variable": variable,
|
| 2290 |
+
"finding": statement[:600],
|
| 2291 |
+
"source": "NASA POWER structured climate evidence",
|
| 2292 |
+
"status": "reported_signal_not_independently_verified",
|
| 2293 |
+
})
|
| 2294 |
+
return findings[:12]
|
| 2295 |
|
|
|
|
|
|
|
| 2296 |
|
| 2297 |
+
def _react_weather_agent(location, variable, days, openai_api_key, react_log):
|
| 2298 |
+
"""Fixed-window observation followed by non-blocking anomaly interpretation."""
|
| 2299 |
+
selected_days = int(days)
|
| 2300 |
+
action = (f"weather_data_analyst(location={location}, variable={variable}, "
|
| 2301 |
+
f"days={selected_days})")
|
| 2302 |
+
wx_outputs = analyze_weather_ui(location, variable, selected_days)
|
| 2303 |
+
data = _weather_data_from_ui_outputs(wx_outputs)
|
| 2304 |
+
status = str(data.get("status") or "")
|
| 2305 |
+
error = status if status.lstrip().startswith(("❌", "Error:", "Failed")) else ""
|
| 2306 |
if error:
|
| 2307 |
+
data["error"] = error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2308 |
|
| 2309 |
+
checks = {
|
| 2310 |
+
"structured_evidence": bool(str(data.get("structured_evidence") or "").strip()),
|
| 2311 |
+
"data_preview": bool(data.get("data_preview")),
|
| 2312 |
+
"statistical_tests": bool(data.get("stats_table")),
|
| 2313 |
+
"stationarity_diagnostic": bool(str(data.get("stationarity_conclusion") or "").strip()),
|
| 2314 |
+
}
|
| 2315 |
+
alerts = []
|
| 2316 |
+
for name, available in checks.items():
|
| 2317 |
+
if not available:
|
| 2318 |
+
alerts.append(f"Unavailable diagnostic: {name}")
|
| 2319 |
+
_react_step(react_log, "WeatherAgent", "Retrieve historical evidence once, keeping the requested window.",
|
| 2320 |
+
action, "Historical evidence received." if not error else f"Service issue: {error}")
|
| 2321 |
+
|
| 2322 |
+
# Bounded local interpretation loop: identify, classify, report; no API retries.
|
| 2323 |
+
anomalies = _extract_weather_anomalies(data.get("structured_evidence"), variable)
|
| 2324 |
+
for finding in anomalies:
|
| 2325 |
+
_react_step(react_log, "WeatherAgent", "Review explicit climate anomaly evidence.",
|
| 2326 |
+
"inspect_structured_evidence", finding["finding"])
|
| 2327 |
+
if anomalies:
|
| 2328 |
+
alerts.append(f"{len(anomalies)} potential weather anomaly signal(s) in structured evidence.")
|
| 2329 |
+
conclusion = str(data.get("stationarity_conclusion") or "")
|
| 2330 |
+
if _is_nonstationary(conclusion):
|
| 2331 |
+
alerts.append("Non-stationarity reported by historical diagnostics (not itself a weather anomaly).")
|
| 2332 |
+
if error:
|
| 2333 |
+
alerts.append(f"Historical service unavailable: {error}")
|
| 2334 |
+
|
| 2335 |
+
data["weather_anomalies"] = anomalies
|
| 2336 |
+
data["weather_alerts"] = alerts
|
| 2337 |
+
data["evidence_quality"] = {
|
| 2338 |
+
**checks,
|
| 2339 |
+
"complete": all(checks.values()) and not bool(error),
|
| 2340 |
+
"selected_window_days": selected_days,
|
| 2341 |
+
"attempted_windows_days": [selected_days],
|
| 2342 |
+
"non_blocking": True,
|
| 2343 |
+
"anomaly_detection_method": "explicit statements in upstream structured evidence",
|
| 2344 |
+
}
|
| 2345 |
+
_react_step(react_log, "WeatherAgent", "Report anomalies and continue downstream.",
|
| 2346 |
+
"report_weather_anomalies", "; ".join(alerts) if alerts else "No explicit anomaly signals; continue.")
|
| 2347 |
+
return data, wx_outputs, selected_days
|
| 2348 |
|
| 2349 |
|
| 2350 |
def _react_satellite_agent(location, sat_variable, days, max_cloud, openai_api_key, react_log):
|
|
|
|
| 2595 |
]
|
| 2596 |
return "\n".join(lines)
|
| 2597 |
|
| 2598 |
+
def _report_visual_evidence(wx_outputs, sat_outputs, fc_outputs, openai_api_key):
|
| 2599 |
+
"""Inspect actual chart pixels with a multimodal model, not only numeric captions."""
|
| 2600 |
+
import base64
|
| 2601 |
+
import io
|
| 2602 |
+
import requests
|
| 2603 |
+
from PIL import Image
|
| 2604 |
+
charts = [
|
| 2605 |
+
("Historical climate context", wx_outputs, 2),
|
| 2606 |
+
("Historical anomalies", wx_outputs, 3),
|
| 2607 |
+
("Satellite true-colour scene", sat_outputs, 2),
|
| 2608 |
+
("Satellite cloud distribution", sat_outputs, 4),
|
| 2609 |
+
("Satellite land cover", sat_outputs, 5),
|
| 2610 |
+
("Satellite spectral false-colour gallery", sat_outputs, 12),
|
| 2611 |
+
("Satellite spectral correlation", sat_outputs, 16),
|
| 2612 |
+
("Satellite spectral time series", sat_outputs, 14),
|
| 2613 |
+
("Satellite spectral deviations", sat_outputs, 15),
|
| 2614 |
+
("Forecast history and outlook", fc_outputs, 4),
|
| 2615 |
+
("Forecast backtest", fc_outputs, 7),
|
| 2616 |
+
]
|
| 2617 |
+
content = [{"type": "input_text", "text": (
|
| 2618 |
+
"Inspect the attached climate plots as visual evidence. For each chart, describe "
|
| 2619 |
+
"the actual visual pattern (direction, timing, peaks, anomalies, intervals, gaps), "
|
| 2620 |
+
"distinguish what is visually supported from uncertain interpretation, and "
|
| 2621 |
+
"identify any disagreement between charts. For satellite true-colour and false-colour "
|
| 2622 |
+
"imagery, distinguish visible water, vegetation, exposed soil, snow and clouds "
|
| 2623 |
+
"only when supported by the image and spectral indices. Explain spatial patterns, "
|
| 2624 |
+
"possible land-cover change and image-quality limitations (clouds, sampling, resolution), "
|
| 2625 |
+
"without treating colour alone as a verified land-cover classification. "
|
| 2626 |
+
"Do not infer flood, fire, drought, damage or a temporal change from a single scene. "
|
| 2627 |
+
"Cross-check visual impressions against NDVI, NDWI, NDSI, BSI, metadata and dates. "
|
| 2628 |
+
"Do not invent chart values, "
|
| 2629 |
+
"geographical attribution, causal claims or official warnings. "
|
| 2630 |
+
"Return concise observations with the chart titles."
|
| 2631 |
+
)}]
|
| 2632 |
+
included = []
|
| 2633 |
+
for title, outputs, index in charts:
|
| 2634 |
+
if not outputs or len(outputs) <= index:
|
| 2635 |
+
continue
|
| 2636 |
+
raw = outputs[index]
|
| 2637 |
+
if raw is None:
|
| 2638 |
+
continue
|
| 2639 |
+
try:
|
| 2640 |
+
import numpy as np
|
| 2641 |
+
if isinstance(raw, np.ndarray):
|
| 2642 |
+
img = Image.fromarray(raw.astype('uint8')).convert('RGB')
|
| 2643 |
+
elif isinstance(raw, str):
|
| 2644 |
+
img = Image.open(raw).convert('RGB')
|
| 2645 |
+
else:
|
| 2646 |
+
continue
|
| 2647 |
+
img.thumbnail((1100, 750))
|
| 2648 |
+
buffer = io.BytesIO()
|
| 2649 |
+
img.save(buffer, format='JPEG', quality=76)
|
| 2650 |
+
encoded = base64.b64encode(buffer.getvalue()).decode('ascii')
|
| 2651 |
+
content.append({"type": "input_text", "text": "Chart: " + title})
|
| 2652 |
+
content.append({"type": "input_image", "image_url": "data:image/jpeg;base64," + encoded})
|
| 2653 |
+
included.append(title)
|
| 2654 |
+
except (OSError, ValueError, TypeError):
|
| 2655 |
+
continue
|
| 2656 |
+
if not included:
|
| 2657 |
+
return {"observations": "Visual inspection unavailable: no chart images were accessible.", "charts": []}
|
| 2658 |
+
try:
|
| 2659 |
+
response = requests.post(
|
| 2660 |
+
'https://api.openai.com/v1/responses',
|
| 2661 |
+
headers={'Authorization': f'Bearer {openai_api_key}', 'Content-Type': 'application/json'},
|
| 2662 |
+
json={'model': 'gpt-4.1', 'input': [{'role': 'user', 'content': content}],
|
| 2663 |
+
'max_output_tokens': 1100}, timeout=120,
|
| 2664 |
+
)
|
| 2665 |
+
response.raise_for_status()
|
| 2666 |
+
payload = response.json()
|
| 2667 |
+
observations = '\n'.join(
|
| 2668 |
+
part.get('text', '') for block in payload.get('output', [])
|
| 2669 |
+
for part in block.get('content', []) if part.get('type') == 'output_text'
|
| 2670 |
+
)
|
| 2671 |
+
return {"observations": observations or 'Visual analysis returned no text.', "charts": included}
|
| 2672 |
+
except requests.RequestException as exc:
|
| 2673 |
+
return {"observations": f'Visual inspection unavailable: {exc}', "charts": included}
|
| 2674 |
+
|
| 2675 |
+
|
| 2676 |
+
def _report_react_review(draft, source_prompt, visual_evidence, alert_result, openai_api_key, react_log):
|
| 2677 |
+
"""Bounded ReAct editorial review: inspect, critique, revise, then stop."""
|
| 2678 |
+
current = draft
|
| 2679 |
+
for iteration in range(1, 3):
|
| 2680 |
+
review_prompt = (
|
| 2681 |
+
'Review this climate report as an independent scientific editor. Check factual '
|
| 2682 |
+
'grounding, interpretation of actual figures, specific official alert sources and '
|
| 2683 |
+
'dates, daily forecast alignment, uncertainty, actionable implications and all '
|
| 2684 |
+
'eight requested sections. No fabricated alerts or image measurements. '
|
| 2685 |
+
'Return ONLY JSON with keys verdict (accept or revise), issues (array of strings), '
|
| 2686 |
+
'and priority (one string).\n'
|
| 2687 |
+
'VISUAL EVIDENCE: ' + str(visual_evidence)[:6000] + '\n'
|
| 2688 |
+
'WEB ALERTS: ' + str(alert_result)[:4500] + '\n'
|
| 2689 |
+
'REPORT: ' + current[:18000]
|
| 2690 |
+
)
|
| 2691 |
+
try:
|
| 2692 |
+
raw = _call_gpt(review_prompt, openai_api_key,
|
| 2693 |
+
system='You are a critical, evidence-based climate report reviewer. Return JSON only.',
|
| 2694 |
+
max_output_tokens=1300)
|
| 2695 |
+
parsed = json.loads(raw.strip().removeprefix('```json').removeprefix('```').removesuffix('```').strip())
|
| 2696 |
+
except (ValueError, TypeError, KeyError) as exc:
|
| 2697 |
+
react_log.append({'agent': 'ReportingAgent', 'thought': 'Review response not parseable',
|
| 2698 |
+
'action': 'accept_with_review_limit', 'observation': str(exc)})
|
| 2699 |
+
break
|
| 2700 |
+
issues = parsed.get('issues', [])
|
| 2701 |
+
verdict = parsed.get('verdict', 'revise')
|
| 2702 |
+
react_log.append({'agent': 'ReportingAgent', 'thought': f'Editorial review {iteration}',
|
| 2703 |
+
'action': 'accept' if verdict == 'accept' else 'revise_draft',
|
| 2704 |
+
'observation': '; '.join(map(str, issues))[:1000] or 'No material issues found'})
|
| 2705 |
+
if verdict == 'accept' or not issues:
|
| 2706 |
+
break
|
| 2707 |
+
revision_prompt = (
|
| 2708 |
+
'Revise the following climate report to resolve the editorial findings. '
|
| 2709 |
+
'Preserve the eight-section structure and all grounded quantitative values. '
|
| 2710 |
+
'Explain observed visual patterns and dated web alerts only where verified; '
|
| 2711 |
+
'never invent a chart value, a warning or a source URL. '
|
| 2712 |
+
'Avoid merely listing numbers: explain implications and contradictions. '
|
| 2713 |
+
'Return the COMPLETE revised Markdown report.\n'
|
| 2714 |
+
'ISSUES: ' + json.dumps(issues, ensure_ascii=False) + '\n'
|
| 2715 |
+
'VISUAL EVIDENCE: ' + str(visual_evidence)[:6000] + '\n'
|
| 2716 |
+
'ALERT SOURCES: ' + str(alert_result)[:4500] + '\n'
|
| 2717 |
+
'ORIGINAL SOURCE CONTEXT: ' + source_prompt[:12500] + '\n'
|
| 2718 |
+
'CURRENT REPORT: ' + current[:18000]
|
| 2719 |
+
)
|
| 2720 |
+
try:
|
| 2721 |
+
revised = _call_gpt(revision_prompt, openai_api_key,
|
| 2722 |
+
system='You are a climate-risk reporting agent. Preserve facts and citations.',
|
| 2723 |
+
max_output_tokens=5600)
|
| 2724 |
+
if revised.strip():
|
| 2725 |
+
current = revised
|
| 2726 |
+
except Exception as exc:
|
| 2727 |
+
react_log.append({'agent': 'ReportingAgent', 'thought': 'Revision failed',
|
| 2728 |
+
'action': 'retain_previous_draft', 'observation': str(exc)})
|
| 2729 |
+
break
|
| 2730 |
+
return current
|
| 2731 |
+
|
| 2732 |
+
|
| 2733 |
def generate_risk_report_ui(
|
| 2734 |
location, variable, forecast_model, days, satellite_days, max_cloud, sat_variable
|
| 2735 |
):
|
|
|
|
| 2964 |
forecast_evidence = forecast_data.get("structured_evidence") or "Unavailable"
|
| 2965 |
consistency = satellite_data.get("metadata_spectral_consistency", [])
|
| 2966 |
|
| 2967 |
+
# Reporting Agent tools: inspect real chart pixels and evaluate web-alert coverage.
|
| 2968 |
+
status_log.append("🔎 [ReportingAgent] Inspecting charts and alert evidence…")
|
| 2969 |
+
yield _emit(status_log, wx=wx_outputs, sat=sat_outputs, fc=fc_outputs)
|
| 2970 |
+
visual_evidence = _report_visual_evidence(wx_outputs, sat_outputs, fc_outputs, openai_api_key)
|
| 2971 |
+
react_log.append({"agent": "ReportingAgent", "thought": "Read actual plots and satellite scenes before drafting",
|
| 2972 |
+
"action": "inspect_chart_images",
|
| 2973 |
+
"observation": f"Inspected {len(visual_evidence['charts'])} charts. "
|
| 2974 |
+
+ visual_evidence['observations'][:500]})
|
| 2975 |
+
alert_result = pplx_news_result
|
| 2976 |
+
if pplx_api_key and not pplx_news_result.get('citations'):
|
| 2977 |
+
try:
|
| 2978 |
+
alert_result = _call_perplexity(
|
| 2979 |
+
f"Find dated official active or recently issued severe weather alerts for {location} "
|
| 2980 |
+
f"from national meteorological services, civil protection or WMO-linked agencies. "
|
| 2981 |
+
"Give issuer, warning type, validity dates, exact source URLs, and distinguish "
|
| 2982 |
+
"confirmed warnings from press reporting. If unverified, explicitly say so.",
|
| 2983 |
+
pplx_api_key, recency='week')
|
| 2984 |
+
react_log.append({"agent": "ReportingAgent", "thought": "Initial alert lookup lacked citations",
|
| 2985 |
+
"action": "search_official_weather_alerts",
|
| 2986 |
+
"observation": f"Returned {len(alert_result.get('citations', []))} citations"})
|
| 2987 |
+
except Exception as exc:
|
| 2988 |
+
react_log.append({"agent": "ReportingAgent", "thought": "Supplementary search unavailable",
|
| 2989 |
+
"action": "continue_without_verified_alerts", "observation": str(exc)})
|
| 2990 |
+
status_log[-1] = "✅ [ReportingAgent] Visual and web evidence reviewed."
|
| 2991 |
+
|
| 2992 |
synthesis_prompt = f"""
|
| 2993 |
You are a senior climate-risk analyst. Produce a concise, evidence-grounded report for **{location}**.
|
| 2994 |
|
|
|
|
| 3007 |
{historical_evidence}
|
| 3008 |
Diagnostic stationarity note: {weather_data.get('stationarity_conclusion', 'N/A')}
|
| 3009 |
Historical data error: {weather_data.get('error', 'none')}
|
| 3010 |
+
Historical weather anomaly signals (unverified statements from structured evidence): {json.dumps(weather_data.get('weather_anomalies', []), ensure_ascii=False)}
|
| 3011 |
+
Historical evidence alerts: {json.dumps(weather_data.get('weather_alerts', []), ensure_ascii=False)}
|
| 3012 |
+
- Distinguish meteorological anomalies from stationarity diagnostics. Do not invent anomaly thresholds or treat unverified signals as confirmed events.
|
| 3013 |
|
| 3014 |
## SATELLITE INDEX GLOSSARY — PLAIN LANGUAGE
|
| 3015 |
- NDVI (Normalized Difference Vegetation Index): vegetation greenness/health; higher generally means denser, more active vegetation.
|
|
|
|
| 3057 |
Use these rows exactly in the 7-Day Outlook; do not recalculate the differences.
|
| 3058 |
{json.dumps(daily_outlook_comparison, ensure_ascii=False)}
|
| 3059 |
|
| 3060 |
+
## ADDITIONAL REPORTING AGENT EVIDENCE
|
| 3061 |
+
Actual chart AND Sentinel-2 scene inspection (qualitative; do not invent numeric values):
|
| 3062 |
+
{visual_evidence['observations']}
|
| 3063 |
+
Charts inspected: {', '.join(visual_evidence['charts']) or 'none'}
|
| 3064 |
+
Official alert search and citations (unverified alerts must not be presented as confirmed):
|
| 3065 |
+
{json.dumps(alert_result, ensure_ascii=False)[:6500]}
|
| 3066 |
+
|
| 3067 |
## E) RECENT / UPCOMING EXTERNAL EVENT INTELLIGENCE
|
| 3068 |
{str(pplx_news_result.get('content') or 'External event intelligence unavailable.')[:1600]}
|
| 3069 |
|
|
|
|
| 3202 |
if missing_report.strip():
|
| 3203 |
gpt_report = gpt_report.rstrip() + "\n\n---\n\n" + missing_report.strip()
|
| 3204 |
|
| 3205 |
+
react_log.append({"agent": "ReportingAgent", "thought": "Integrate numerical, visual and external evidence",
|
| 3206 |
+
"action": "draft_report", "observation": "Initial eight-section report drafted"})
|
| 3207 |
+
gpt_report = _report_react_review(
|
| 3208 |
+
gpt_report, synthesis_prompt, visual_evidence, alert_result, openai_api_key, react_log)
|
| 3209 |
+
status_log[-1] = "✅ [6/6] Reporting Agent ReAct review complete."
|
| 3210 |
except Exception as exc:
|
| 3211 |
status_log[-1] = f"⚠️ [6/6] GPT error: {exc}"
|
| 3212 |
gpt_report = f"GPT synthesis failed: {exc}"
|
|
|
|
| 3239 |
gr.Markdown(
|
| 3240 |
"Orchestrates Historical, Sentinel-2 and Forecast agents, compares the statistical "
|
| 3241 |
"forecast with **Open-Meteo as an operational reference forecast**, and queries "
|
| 3242 |
+
"**Perplexity** for recent/upcoming extreme-event intelligence. The Reporting Agent reviews the "
|
| 3243 |
"already-structured evidence; it does not create numeric risk scores.\n\n"
|
| 3244 |
"_The report is hazard-focused: exposure and vulnerability are not modelled. "
|
| 3245 |
"OpenAI and Perplexity credentials are loaded server-side from Hugging Face Space Secrets._"
|
|
|
|
| 3304 |
rp_forecast_summary = gr.State("")
|
| 3305 |
|
| 3306 |
with gr.Tabs():
|
| 3307 |
+
with gr.Tab("📋 Hazard-Focused Report (ReAct Agent)"):
|
| 3308 |
rp_gpt_report = gr.Markdown(label="Full Report")
|
| 3309 |
|
| 3310 |
with gr.Tab("📡 Operational Reference (Open-Meteo)"):
|