osint-dashboard/news/summerizer/intel.py
Sirius DevOps 1c47ecbc5a feat: load k8s news prompts from env/files, not Python
Bundled MAP_PROMPT/SUMMARY_PROMPT from the customer1 deepseek
configmap. Env wins over prompt files; blank compose injection
is treated as unset. Parser maps market_overview JSON onto the
dashboard ticker/map contract.
2026-08-28 20:04:47 -04:00

206 lines
6.9 KiB
Python

"""Pure parser for the news-summarizer reduce JSON / geo / importance contract."""
from __future__ import annotations
import json
import re
_EMPTY = {"summary_en": "", "ticker": [], "map_items": []}
_KEEP = frozenset({"critical", "high"})
_THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
_FENCE_RE = re.compile(r"```(?:json)?", re.IGNORECASE)
TICKER_HEADLINE_MAX = 140
MAP_HEADLINE_MAX = 160
TICKER_CAP = 12
MAP_CAP = 20
def _impact_to_importance(level) -> str | None:
lv = str(level or "").strip().lower()
if lv in {"high", "critical"}:
return "critical" if lv == "critical" else "high"
if lv == "medium":
return "high"
return None
def _from_market_intel(data: dict) -> dict:
"""Map the k8s deepseek reduce JSON onto summary_en / ticker / map_items."""
raw_ov = data.get("market_overview")
ov: dict = raw_ov if isinstance(raw_ov, dict) else {}
raw_geo = data.get("geopolitical_osint")
geo: dict = raw_geo if isinstance(raw_geo, dict) else {}
raw_events = ov.get("critical_events")
events: list = raw_events if isinstance(raw_events, list) else []
raw_themes = ov.get("key_themes")
themes: list = raw_themes if isinstance(raw_themes, list) else []
raw_conflicts = geo.get("active_conflicts")
conflicts: list = raw_conflicts if isinstance(raw_conflicts, list) else []
parts: list[str] = []
sentiment = ov.get("overall_sentiment")
if sentiment:
score = ov.get("sentiment_score", "")
parts.append(f"**Sentiment:** {sentiment}" + (f" ({score})" if score != "" else ""))
if themes:
parts.append("**Themes:** " + ", ".join(str(t) for t in themes[:5] if t))
for ev in events[:8]:
if not isinstance(ev, dict):
continue
headline = (ev.get("headline") or "").strip()
desc = (ev.get("description") or "").strip()
if headline and desc:
parts.append(f"### {headline}\n{desc}")
elif headline:
parts.append(f"### {headline}")
elif desc:
parts.append(desc)
for c in conflicts[:4]:
if not isinstance(c, dict):
continue
intel = (c.get("intelligence") or "").strip()
region = (c.get("region") or "").strip()
if intel:
parts.append(f"**{region or 'Conflict'}:** {intel}")
summary_en = "\n\n".join(parts)
ticker: list[dict] = []
for ev in events:
if not isinstance(ev, dict):
continue
importance = _impact_to_importance(ev.get("impact_level"))
if not importance:
continue
headline = (ev.get("headline") or "").strip()
if not headline:
continue
ticker.append({
"headline": headline,
"importance": importance,
"url": ev.get("url") or "",
"location_name": ev.get("location_name") or ev.get("region") or "",
})
map_items: list[dict] = []
for c in conflicts:
if not isinstance(c, dict):
continue
status = str(c.get("status") or "").lower()
importance = "critical" if status == "escalating" else "high"
headline = (c.get("intelligence") or c.get("region") or "").strip()
if not headline:
continue
map_items.append({
"headline": headline,
"importance": importance,
"location_name": c.get("location_name") or c.get("region") or "",
"lat": c.get("lat"),
"lon": c.get("lon"),
"location_confidence": "region",
"category": "military/conflict",
"url": "",
})
for ev in events:
if not isinstance(ev, dict):
continue
importance = _impact_to_importance(ev.get("impact_level"))
if not importance:
continue
if clamp_coords(ev.get("lat"), ev.get("lon")) is None:
continue
headline = (ev.get("headline") or "").strip()
if not headline:
continue
map_items.append({
"headline": headline,
"importance": importance,
"location_name": ev.get("location_name") or "",
"lat": ev.get("lat"),
"lon": ev.get("lon"),
"location_confidence": "city",
"category": ev.get("category") or "other",
"url": ev.get("url") or "",
})
return {"summary_en": summary_en, "ticker": ticker, "map_items": map_items}
def parse_reduce_json(raw: str) -> dict:
try:
text = _THINK_RE.sub("", raw or "")
text = _FENCE_RE.sub("", text)
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1 or end < start:
return dict(_EMPTY)
data = json.loads(text[start : end + 1])
if not isinstance(data, dict):
return dict(_EMPTY)
if isinstance(data.get("summary_en"), str) or isinstance(data.get("ticker"), list):
summary = data.get("summary_en", "")
ticker = data.get("ticker", [])
map_items = data.get("map_items", [])
return {
"summary_en": summary if isinstance(summary, str) else "",
"ticker": ticker if isinstance(ticker, list) else [],
"map_items": map_items if isinstance(map_items, list) else [],
}
if "market_overview" in data or "geopolitical_osint" in data:
return _from_market_intel(data)
return dict(_EMPTY)
except Exception:
return dict(_EMPTY)
def clamp_coords(lat, lon) -> tuple[float, float] | None:
try:
lat_f = float(lat)
lon_f = float(lon)
except (TypeError, ValueError):
return None
if not (-90 <= lat_f <= 90 and -180 <= lon_f <= 180):
return None
return (lat_f, lon_f)
def _trimmed_headline(row: dict, limit: int) -> str:
headline = row.get("headline") or ""
if not isinstance(headline, str):
headline = str(headline)
return headline.strip()[:limit]
def select_ticker(rows: list) -> list:
out = []
for row in rows:
if row.get("importance") not in _KEEP:
continue
headline = _trimmed_headline(row, TICKER_HEADLINE_MAX)
if not headline:
continue
item = dict(row)
item["headline"] = headline
out.append(item)
if len(out) >= TICKER_CAP:
break
return out
def select_map(items: list) -> list:
out = []
for row in items:
if row.get("importance") not in _KEEP:
continue
headline = _trimmed_headline(row, MAP_HEADLINE_MAX)
if not headline:
continue
coords = clamp_coords(row.get("lat"), row.get("lon"))
if coords is None:
continue
item = dict(row)
item["headline"] = headline
item["lat"], item["lon"] = coords
out.append(item)
if len(out) >= MAP_CAP:
break
return out