feat: NLP intelligence — entity extraction, classification, clustering, spikes (+4 = 64 tools)

Phase 10: News Intelligence & NLP
- intel_extract_entities: Regex-based NER for countries, leaders, orgs,
  companies, CVEs, APT groups (28 leaders, 41 orgs, 25 companies, 36 APTs)
- intel_classify_event: Keyword-based threat classification into 14
  categories with severity scoring (1-10)
- intel_news_clusters: Jaccard similarity topic clustering for news
  articles with keyword extraction
- intel_keyword_spikes: Welford's algorithm baseline comparison with
  CVE/APT pattern extraction from headlines

No new dependencies — all pure Python with regex and SQLite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marc Shade
2026-02-24 08:41:40 -05:00
co-authored by Claude Opus 4.6
parent 2aa5fd1425
commit 69f1a6d44f
6 changed files with 835 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
"""Keyword-based event threat classification.
Fast classifier with 14 categories and severity scoring. No ML deps.
Designed as the first pass; an LLM can refine confidence later.
"""
from __future__ import annotations
import re
from datetime import datetime, timezone
# ---------------------------------------------------------------------------
# Category keyword patterns (ordered by specificity)
# ---------------------------------------------------------------------------
CATEGORIES: dict[str, dict] = {
"nuclear": {
"keywords": [
"nuclear", "atomic", "uranium", "plutonium", "warhead",
"icbm", "ballistic missile", "nuclear test", "radiation",
"enrichment", "nonproliferation", "iaea", "dirty bomb",
],
"severity_base": 9,
},
"terrorism": {
"keywords": [
"terrorist", "terrorism", "suicide bomb", "car bomb", "ied",
"hostage", "kidnapping", "beheading", "mass shooting",
"lone wolf", "radicalization", "jihad", "extremist",
],
"severity_base": 9,
},
"military": {
"keywords": [
"military", "troops", "soldier", "artillery", "airstrike",
"bombing", "invasion", "offensive", "deployed", "nato",
"warship", "submarine", "fighter jet", "drone strike",
"mobilization", "ceasefire", "frontline", "casualties",
],
"severity_base": 8,
},
"cyber": {
"keywords": [
"cyber", "hack", "ransomware", "malware", "phishing",
"data breach", "ddos", "vulnerability", "exploit", "cve-",
"zero-day", "apt", "botnet", "credential", "encryption",
],
"severity_base": 7,
},
"political": {
"keywords": [
"election", "coup", "protest", "demonstration", "sanctions",
"embargo", "diplomatic", "parliament", "congress",
"impeachment", "referendum", "regime", "martial law",
"authoritarian", "democracy", "opposition",
],
"severity_base": 6,
},
"economic": {
"keywords": [
"recession", "inflation", "default", "debt crisis",
"currency collapse", "bank run", "stock crash", "layoffs",
"trade war", "tariff", "supply chain", "shortage",
],
"severity_base": 6,
},
"health": {
"keywords": [
"pandemic", "epidemic", "outbreak", "virus", "pathogen",
"vaccine", "quarantine", "who emergency", "mortality",
"infection", "ebola", "bird flu", "h5n1", "mpox",
],
"severity_base": 7,
},
"climate": {
"keywords": [
"hurricane", "typhoon", "cyclone", "earthquake", "tsunami",
"flood", "drought", "wildfire", "heatwave", "glacier",
"sea level", "emission", "carbon", "climate change",
],
"severity_base": 6,
},
"infrastructure": {
"keywords": [
"power outage", "blackout", "pipeline", "cable cut",
"internet outage", "grid failure", "dam collapse",
"bridge collapse", "port blockade", "supply disruption",
],
"severity_base": 7,
},
"maritime": {
"keywords": [
"shipping", "strait", "piracy", "naval blockade", "vessel",
"cargo ship", "tanker", "port", "chokepoint", "seafarer",
"coast guard", "maritime security",
],
"severity_base": 5,
},
"aviation": {
"keywords": [
"airspace", "no-fly zone", "intercept", "air defense",
"airline", "crash", "hijack", "flight ban",
],
"severity_base": 6,
},
"energy": {
"keywords": [
"oil price", "opec", "gas pipeline", "lng", "refinery",
"energy crisis", "power grid", "renewable", "solar",
"wind farm", "battery", "hydrogen",
],
"severity_base": 5,
},
"social_unrest": {
"keywords": [
"riot", "looting", "tear gas", "water cannon", "curfew",
"state of emergency", "civil disobedience", "strike",
"labor dispute", "food riot", "uprising",
],
"severity_base": 7,
},
"space": {
"keywords": [
"satellite", "space debris", "solar flare", "geomagnetic",
"space weather", "orbit", "launch", "rocket", "asteroid",
],
"severity_base": 4,
},
}
# Severity modifiers — keywords that bump severity up
_HIGH_SEVERITY: list[str] = [
"killed", "dead", "death toll", "massacre", "war crime", "genocide",
"nuclear", "catastrophic", "critical", "emergency", "mass casualty",
"destroyed", "collapse", "unprecedented", "worst",
]
_MODERATE_SEVERITY: list[str] = [
"injured", "wounded", "escalation", "crisis", "threat", "warning",
"attack", "strike", "explosion", "breach", "violation",
]
def classify_event(text: str) -> dict:
"""Classify text into threat categories with severity scoring.
Returns dict with primary category, severity (1-10), confidence,
all matched categories, and matched keywords.
"""
text_lower = text.lower()
matches: list[dict] = []
for cat_name, cat_info in CATEGORIES.items():
matched_kw = [kw for kw in cat_info["keywords"] if kw in text_lower]
if matched_kw:
# Confidence scales with number of keyword hits
confidence = min(1.0, len(matched_kw) * 0.2 + 0.3)
matches.append({
"category": cat_name,
"keywords_matched": matched_kw,
"keyword_count": len(matched_kw),
"severity_base": cat_info["severity_base"],
"confidence": round(confidence, 2),
})
# Sort by keyword count (most specific match first)
matches.sort(key=lambda m: m["keyword_count"], reverse=True)
# Compute severity modifier
severity_mod = 0
high_hits = [kw for kw in _HIGH_SEVERITY if kw in text_lower]
mod_hits = [kw for kw in _MODERATE_SEVERITY if kw in text_lower]
if high_hits:
severity_mod = 2
elif mod_hits:
severity_mod = 1
primary = matches[0] if matches else None
severity = min(10, (primary["severity_base"] + severity_mod)) if primary else 0
return {
"primary_category": primary["category"] if primary else "unclassified",
"severity": severity,
"confidence": primary["confidence"] if primary else 0.0,
"all_categories": [
{"category": m["category"], "confidence": m["confidence"], "keywords": m["keywords_matched"]}
for m in matches
],
"category_count": len(matches),
"severity_modifiers": high_hits + mod_hits,
"source": "keyword-classifier",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
async def fetch_classify_event(fetcher, text: str) -> dict:
"""Classify a text event. Thin async wrapper for MCP dispatch."""
return classify_event(text)
@@ -96,3 +96,77 @@ def cluster_articles(
# Sort by member count descending
clusters.sort(key=lambda c: c["member_count"], reverse=True)
return clusters
# ---------------------------------------------------------------------------
# MCP-facing async wrapper
# ---------------------------------------------------------------------------
async def fetch_news_clusters(
fetcher,
category: str | None = None,
limit: int = 100,
threshold: float = 0.25,
) -> dict:
"""Fetch recent news and cluster by topic similarity.
Args:
fetcher: HTTP fetcher instance.
category: Optional RSS feed category filter.
limit: Max news items to fetch.
threshold: Jaccard similarity threshold (0.0 - 1.0).
Returns:
Dict with clusters[], cluster_count, total_items, singleton_count, source.
"""
from datetime import datetime, timezone
from ..sources import news
feed_data = await news.fetch_news_feed(fetcher, category=category, limit=limit)
items = feed_data.get("items", [])
raw_clusters = cluster_articles(items, similarity_threshold=threshold)
out_clusters = []
for c in raw_clusters:
if c["member_count"] <= 1:
continue
rep = c["representative"]
# Collect keywords from all member titles
all_tokens: set[str] = set()
member_items = []
for idx in c["member_indices"]:
art = items[idx]
all_tokens |= _tokenize(art.get("title", ""))
member_items.append({
"title": art.get("title", ""),
"source": art.get("source_name", art.get("source", "")),
"link": art.get("link", ""),
})
# Top keywords by frequency
word_freq: dict[str, int] = {}
for idx in c["member_indices"]:
for w in _tokenize(items[idx].get("title", "")):
word_freq[w] = word_freq.get(w, 0) + 1
top_kw = sorted(word_freq, key=word_freq.get, reverse=True)[:8] # type: ignore[arg-type]
out_clusters.append({
"headline": rep.get("title", ""),
"size": c["member_count"],
"keywords": top_kw,
"sources": list({m["source"] for m in member_items if m["source"]}),
"items": member_items[:5],
})
singletons = sum(1 for c in raw_clusters if c["member_count"] == 1)
return {
"clusters": out_clusters,
"cluster_count": len(out_clusters),
"total_items": len(items),
"singleton_count": singletons,
"threshold": threshold,
"source": "jaccard-clustering",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
+174
View File
@@ -0,0 +1,174 @@
"""Named entity extraction from text.
Regex-based NER for countries, leaders, organizations, companies, CVEs,
and APT groups. No ML dependencies — uses reference data from config/.
"""
from __future__ import annotations
import re
from datetime import datetime, timezone
from ..config.countries import TIER1_COUNTRIES
from ..config.entities import LEADERS, ORGANIZATIONS, COMPANIES, APT_GROUPS
# Pre-compile patterns
_CVE_RE = re.compile(r"CVE-\d{4}-\d{4,}", re.IGNORECASE)
_APT_RE = re.compile(
r"\b(?:" + "|".join(re.escape(a) for a in sorted(APT_GROUPS, key=len, reverse=True)) + r")\b",
re.IGNORECASE,
)
# Build country keyword → iso3 lookup (lowercase)
_COUNTRY_KW: dict[str, str] = {}
for _iso3, _info in TIER1_COUNTRIES.items():
for _kw in _info["keywords"]:
_COUNTRY_KW[_kw.lower()] = _iso3
_COUNTRY_KW[_info["name"].lower()] = _iso3
_COUNTRY_KW[_iso3.lower()] = _iso3
# Build leader name lookup (lowercase)
_LEADER_KW: dict[str, dict] = {k.lower(): v for k, v in LEADERS.items()}
# Build org lookup (lowercase)
_ORG_KW: dict[str, dict] = {k.lower(): v for k, v in ORGANIZATIONS.items()}
# Build company lookup (lowercase)
_COMPANY_KW: dict[str, dict] = {k.lower(): v for k, v in COMPANIES.items()}
def extract_entities(text: str) -> dict:
"""Extract named entities from text.
Returns dict with entities grouped by type, plus counts and metadata.
"""
text_lower = text.lower()
words_set = set(text_lower.split())
countries: list[dict] = []
leaders: list[dict] = []
organizations: list[dict] = []
companies: list[dict] = []
cves: list[str] = []
apts: list[str] = []
seen_countries: set[str] = set()
seen_leaders: set[str] = set()
seen_orgs: set[str] = set()
seen_companies: set[str] = set()
# Countries (keyword search in text)
for kw, iso3 in _COUNTRY_KW.items():
if iso3 in seen_countries:
continue
if kw in text_lower:
seen_countries.add(iso3)
info = TIER1_COUNTRIES[iso3]
countries.append({
"iso3": iso3,
"name": info["name"],
"baseline_risk": info["baseline_risk"],
})
# Leaders (match longest first to avoid partial matches)
for kw in sorted(_LEADER_KW.keys(), key=len, reverse=True):
info = _LEADER_KW[kw]
if info["name"] in seen_leaders:
continue
if kw in text_lower:
seen_leaders.add(info["name"])
leaders.append({
"name": info["name"],
"title": info["title"],
"country": info["country"],
})
# Organizations
for kw in sorted(_ORG_KW.keys(), key=len, reverse=True):
info = _ORG_KW[kw]
if info["abbrev"] in seen_orgs:
continue
# For short abbreviations (2-4 chars), require word boundary
if len(kw) <= 4:
if kw in words_set:
seen_orgs.add(info["abbrev"])
organizations.append({
"name": info["abbrev"],
"type": info["type"],
})
elif kw in text_lower:
seen_orgs.add(info["abbrev"])
organizations.append({
"name": info["abbrev"],
"type": info["type"],
})
# Companies
for kw in sorted(_COMPANY_KW.keys(), key=len, reverse=True):
info = _COMPANY_KW[kw]
if kw in seen_companies:
continue
if kw in text_lower:
seen_companies.add(kw)
companies.append({
"name": kw.title(),
"ticker": info["ticker"],
"sector": info["sector"],
})
# CVEs
cves = list(set(_CVE_RE.findall(text)))
# APTs
apt_matches = _APT_RE.findall(text)
apts = list(set(m.lower() for m in apt_matches))
total = len(countries) + len(leaders) + len(organizations) + len(companies) + len(cves) + len(apts)
return {
"entities": {
"countries": countries,
"leaders": leaders,
"organizations": organizations,
"companies": companies,
"cves": cves,
"apt_groups": apts,
},
"by_type": {
"countries": len(countries),
"leaders": len(leaders),
"organizations": len(organizations),
"companies": len(companies),
"cves": len(cves),
"apt_groups": len(apts),
},
"total_entities": total,
"source": "regex-ner",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
async def fetch_entity_extraction(fetcher, text: str | None = None, use_news: bool = True) -> dict:
"""Extract entities from provided text or recent news headlines.
If text is provided, extract from that. Otherwise fetch recent news
headlines and extract entities from the combined text.
"""
if text:
return extract_entities(text)
if use_news:
from ..sources import news
feed_data = await news.fetch_news_feed(fetcher, limit=100)
items = feed_data.get("items", [])
combined = " ".join(
(item.get("title", "") + " " + item.get("summary", ""))
for item in items
)
result = extract_entities(combined)
result["input_source"] = "news_feed"
result["items_analyzed"] = len(items)
return result
return extract_entities("")
+187
View File
@@ -0,0 +1,187 @@
"""Keyword spike detection with CVE/APT extraction.
Compares recent keyword frequencies against stored baselines to detect
abnormal surges. Uses SQLite for baseline persistence (like temporal.py).
"""
from __future__ import annotations
import logging
import os
import re
import sqlite3
from datetime import datetime, timezone
logger = logging.getLogger("world-intel-mcp.analysis.spikes")
_DB_PATH = os.path.join(
os.path.expanduser("~"), ".cache", "world-intel-mcp", "keyword_spikes.db"
)
_CVE_RE = re.compile(r"CVE-\d{4}-\d{4,}", re.IGNORECASE)
_APT_RE = re.compile(
r"\b(APT\d{1,3}|Lazarus|Fancy Bear|Cozy Bear|Sandworm|Turla|Kimsuky|"
r"Volt Typhoon|Salt Typhoon|Midnight Blizzard|Scattered Spider|"
r"LockBit|BlackCat|ALPHV|CL0P|Black Basta|Charming Kitten)\b",
re.IGNORECASE,
)
class KeywordSpikeDetector:
"""Detects keyword frequency spikes against rolling baselines."""
def __init__(self, db_path: str = _DB_PATH):
os.makedirs(os.path.dirname(db_path), exist_ok=True)
self._conn = sqlite3.connect(db_path)
self._conn.execute(
"""CREATE TABLE IF NOT EXISTS kw_baselines (
keyword TEXT PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0,
mean REAL NOT NULL DEFAULT 0.0,
m2 REAL NOT NULL DEFAULT 0.0,
updated_at TEXT NOT NULL
)"""
)
self._conn.commit()
def _get_baseline(self, keyword: str) -> tuple[int, float, float]:
"""Get (count, mean, m2) for a keyword."""
row = self._conn.execute(
"SELECT count, mean, m2 FROM kw_baselines WHERE keyword = ?",
(keyword,),
).fetchone()
return row if row else (0, 0.0, 0.0)
def _update_baseline(self, keyword: str, value: float) -> None:
"""Welford's online update for running mean/variance."""
n, mean, m2 = self._get_baseline(keyword)
n += 1
delta = value - mean
mean += delta / n
delta2 = value - mean
m2 += delta * delta2
now = datetime.now(timezone.utc).isoformat()
self._conn.execute(
"""INSERT INTO kw_baselines (keyword, count, mean, m2, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(keyword) DO UPDATE SET
count=?, mean=?, m2=?, updated_at=?""",
(keyword, n, mean, m2, now, n, mean, m2, now),
)
self._conn.commit()
def detect_spikes(
self,
keyword_counts: dict[str, int],
z_threshold: float = 2.0,
) -> list[dict]:
"""Compare current keyword counts against baselines.
Args:
keyword_counts: {keyword: count} from current window.
z_threshold: Z-score threshold for spike detection.
Returns:
List of spike dicts sorted by z-score descending.
"""
spikes: list[dict] = []
for kw, current_count in keyword_counts.items():
n, mean, m2 = self._get_baseline(kw)
if n < 3:
# Not enough data for meaningful z-score
self._update_baseline(kw, float(current_count))
continue
variance = m2 / n
stddev = variance ** 0.5
if stddev < 0.1:
# Near-zero variance — use ratio instead
ratio = current_count / max(mean, 0.1)
if ratio > 3.0:
spikes.append({
"keyword": kw,
"current_count": current_count,
"baseline_mean": round(mean, 2),
"ratio": round(ratio, 2),
"z_score": None,
"detection": "ratio",
})
else:
z = (current_count - mean) / stddev
if z >= z_threshold:
spikes.append({
"keyword": kw,
"current_count": current_count,
"baseline_mean": round(mean, 2),
"stddev": round(stddev, 2),
"z_score": round(z, 2),
"ratio": round(current_count / max(mean, 0.1), 2),
"detection": "z_score",
})
# Update baseline
self._update_baseline(kw, float(current_count))
spikes.sort(key=lambda s: s.get("z_score") or s.get("ratio", 0), reverse=True)
return spikes
# Module-level singleton
_detector: KeywordSpikeDetector | None = None
def _ensure_detector() -> KeywordSpikeDetector:
global _detector
if _detector is None:
_detector = KeywordSpikeDetector()
return _detector
async def fetch_keyword_spikes(
fetcher,
min_count: int = 3,
z_threshold: float = 2.0,
) -> dict:
"""Fetch trending keywords and detect spikes against baselines.
Also extracts CVE identifiers and APT group mentions from headlines.
"""
from ..sources import news
# Get current keyword frequencies
kw_data = await news.fetch_trending_keywords(fetcher, min_count=min_count)
keywords = kw_data.get("keywords", [])
# Build count dict
kw_counts: dict[str, int] = {}
for item in keywords:
kw_counts[item["keyword"]] = item["count"]
# Detect spikes
detector = _ensure_detector()
spikes = detector.detect_spikes(kw_counts, z_threshold=z_threshold)
# Extract CVEs and APTs from all recent headlines
feed_data = await news.fetch_news_feed(fetcher, limit=100)
all_text = " ".join(
(it.get("title", "") + " " + it.get("summary", ""))
for it in feed_data.get("items", [])
)
cve_mentions = list(set(_CVE_RE.findall(all_text)))
apt_mentions = list(set(m.lower() for m in _APT_RE.findall(all_text)))
return {
"spikes": spikes,
"spike_count": len(spikes),
"keywords_analyzed": len(kw_counts),
"z_threshold": z_threshold,
"cve_mentions": sorted(cve_mentions),
"apt_mentions": sorted(apt_mentions),
"cve_count": len(cve_mentions),
"apt_count": len(apt_mentions),
"source": "keyword-spike-detector",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
+132
View File
@@ -0,0 +1,132 @@
"""Entity reference data for NER extraction.
Pure data module — no I/O, no external dependencies.
Maps leaders, organizations, companies to normalized forms.
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# World leaders (name variants → normalized key)
# ---------------------------------------------------------------------------
LEADERS: dict[str, dict] = {
"biden": {"name": "Joe Biden", "title": "President", "country": "USA"},
"joe biden": {"name": "Joe Biden", "title": "President", "country": "USA"},
"trump": {"name": "Donald Trump", "title": "Former President", "country": "USA"},
"donald trump": {"name": "Donald Trump", "title": "Former President", "country": "USA"},
"xi jinping": {"name": "Xi Jinping", "title": "President", "country": "CHN"},
"xi": {"name": "Xi Jinping", "title": "President", "country": "CHN"},
"putin": {"name": "Vladimir Putin", "title": "President", "country": "RUS"},
"vladimir putin": {"name": "Vladimir Putin", "title": "President", "country": "RUS"},
"zelensky": {"name": "Volodymyr Zelensky", "title": "President", "country": "UKR"},
"zelenskyy": {"name": "Volodymyr Zelensky", "title": "President", "country": "UKR"},
"macron": {"name": "Emmanuel Macron", "title": "President", "country": "FRA"},
"starmer": {"name": "Keir Starmer", "title": "Prime Minister", "country": "GBR"},
"scholz": {"name": "Olaf Scholz", "title": "Chancellor", "country": "DEU"},
"modi": {"name": "Narendra Modi", "title": "Prime Minister", "country": "IND"},
"narendra modi": {"name": "Narendra Modi", "title": "Prime Minister", "country": "IND"},
"netanyahu": {"name": "Benjamin Netanyahu", "title": "Prime Minister", "country": "ISR"},
"khamenei": {"name": "Ali Khamenei", "title": "Supreme Leader", "country": "IRN"},
"kim jong un": {"name": "Kim Jong Un", "title": "Supreme Leader", "country": "PRK"},
"kim jong-un": {"name": "Kim Jong Un", "title": "Supreme Leader", "country": "PRK"},
"erdogan": {"name": "Recep Tayyip Erdogan", "title": "President", "country": "TUR"},
"kishida": {"name": "Fumio Kishida", "title": "Prime Minister", "country": "JPN"},
"trudeau": {"name": "Justin Trudeau", "title": "Prime Minister", "country": "CAN"},
"lula": {"name": "Luiz Inacio Lula da Silva", "title": "President", "country": "BRA"},
"al-assad": {"name": "Bashar al-Assad", "title": "Former President", "country": "SYR"},
"marcos": {"name": "Ferdinand Marcos Jr.", "title": "President", "country": "PHL"},
"albanese": {"name": "Anthony Albanese", "title": "Prime Minister", "country": "AUS"},
"milei": {"name": "Javier Milei", "title": "President", "country": "ARG"},
"meloni": {"name": "Giorgia Meloni", "title": "Prime Minister", "country": "ITA"},
}
# ---------------------------------------------------------------------------
# International organizations
# ---------------------------------------------------------------------------
ORGANIZATIONS: dict[str, dict] = {
"united nations": {"abbrev": "UN", "type": "intl_org"},
"un": {"abbrev": "UN", "type": "intl_org"},
"nato": {"abbrev": "NATO", "type": "military_alliance"},
"european union": {"abbrev": "EU", "type": "political_bloc"},
"eu": {"abbrev": "EU", "type": "political_bloc"},
"who": {"abbrev": "WHO", "type": "intl_org"},
"world health organization": {"abbrev": "WHO", "type": "intl_org"},
"imf": {"abbrev": "IMF", "type": "financial"},
"international monetary fund": {"abbrev": "IMF", "type": "financial"},
"world bank": {"abbrev": "WB", "type": "financial"},
"opec": {"abbrev": "OPEC", "type": "energy"},
"iaea": {"abbrev": "IAEA", "type": "nuclear"},
"international atomic energy agency": {"abbrev": "IAEA", "type": "nuclear"},
"red cross": {"abbrev": "ICRC", "type": "humanitarian"},
"icrc": {"abbrev": "ICRC", "type": "humanitarian"},
"unhcr": {"abbrev": "UNHCR", "type": "humanitarian"},
"unicef": {"abbrev": "UNICEF", "type": "humanitarian"},
"asean": {"abbrev": "ASEAN", "type": "political_bloc"},
"african union": {"abbrev": "AU", "type": "political_bloc"},
"brics": {"abbrev": "BRICS", "type": "political_bloc"},
"g7": {"abbrev": "G7", "type": "political_bloc"},
"g20": {"abbrev": "G20", "type": "political_bloc"},
"wto": {"abbrev": "WTO", "type": "trade"},
"world trade organization": {"abbrev": "WTO", "type": "trade"},
"hamas": {"abbrev": "Hamas", "type": "militant"},
"hezbollah": {"abbrev": "Hezbollah", "type": "militant"},
"houthis": {"abbrev": "Houthis", "type": "militant"},
"isis": {"abbrev": "ISIS", "type": "militant"},
"islamic state": {"abbrev": "ISIS", "type": "militant"},
"al-qaeda": {"abbrev": "AQ", "type": "militant"},
"al qaeda": {"abbrev": "AQ", "type": "militant"},
"taliban": {"abbrev": "Taliban", "type": "militant"},
"wagner": {"abbrev": "Wagner", "type": "pmc"},
"wagner group": {"abbrev": "Wagner", "type": "pmc"},
"cia": {"abbrev": "CIA", "type": "intelligence"},
"fbi": {"abbrev": "FBI", "type": "intelligence"},
"mossad": {"abbrev": "Mossad", "type": "intelligence"},
"mi6": {"abbrev": "MI6", "type": "intelligence"},
"fsb": {"abbrev": "FSB", "type": "intelligence"},
"pentagon": {"abbrev": "DoD", "type": "military"},
"department of defense": {"abbrev": "DoD", "type": "military"},
}
# ---------------------------------------------------------------------------
# Major companies (defense, tech, energy)
# ---------------------------------------------------------------------------
COMPANIES: dict[str, dict] = {
"lockheed martin": {"ticker": "LMT", "sector": "defense"},
"raytheon": {"ticker": "RTX", "sector": "defense"},
"northrop grumman": {"ticker": "NOC", "sector": "defense"},
"boeing": {"ticker": "BA", "sector": "defense"},
"general dynamics": {"ticker": "GD", "sector": "defense"},
"bae systems": {"ticker": "BA.L", "sector": "defense"},
"rheinmetall": {"ticker": "RHM.DE", "sector": "defense"},
"apple": {"ticker": "AAPL", "sector": "tech"},
"google": {"ticker": "GOOGL", "sector": "tech"},
"alphabet": {"ticker": "GOOGL", "sector": "tech"},
"microsoft": {"ticker": "MSFT", "sector": "tech"},
"amazon": {"ticker": "AMZN", "sector": "tech"},
"meta": {"ticker": "META", "sector": "tech"},
"nvidia": {"ticker": "NVDA", "sector": "tech"},
"openai": {"ticker": None, "sector": "ai"},
"anthropic": {"ticker": None, "sector": "ai"},
"deepmind": {"ticker": None, "sector": "ai"},
"aramco": {"ticker": "2222.SR", "sector": "energy"},
"exxonmobil": {"ticker": "XOM", "sector": "energy"},
"chevron": {"ticker": "CVX", "sector": "energy"},
"shell": {"ticker": "SHEL", "sector": "energy"},
"bp": {"ticker": "BP", "sector": "energy"},
"gazprom": {"ticker": None, "sector": "energy"},
"tsmc": {"ticker": "TSM", "sector": "semiconductor"},
"samsung": {"ticker": "005930.KS", "sector": "tech"},
}
# ---------------------------------------------------------------------------
# APT / threat actor names
# ---------------------------------------------------------------------------
APT_GROUPS: set[str] = {
"apt28", "apt29", "apt30", "apt31", "apt33", "apt34", "apt35", "apt38",
"apt40", "apt41", "lazarus", "lazarus group", "fancy bear", "cozy bear",
"sandworm", "turla", "equation group", "kimsuky", "charming kitten",
"double dragon", "stone panda", "volt typhoon", "salt typhoon",
"midnight blizzard", "star blizzard", "forest blizzard",
"scattered spider", "lapsus$", "lockbit", "blackcat", "alphv",
"cl0p", "conti", "revil", "darkside", "black basta",
}
+70
View File
@@ -16,6 +16,7 @@ Phase 6: Military & infrastructure intelligence (+6 = 45 tools).
Phase 7: Health, sanctions, elections, shipping, social, nuclear, alerts, trends (+10 = 55 tools).
Phase 8: Service status monitoring, RSS expansion (80+ feeds, 14 categories) (+1 = 56 tools).
Phase 9: Geospatial datasets — military bases, ports, pipelines, nuclear facilities (+4 = 60 tools).
Phase 10: NLP intelligence — entity extraction, event classification, news clustering, keyword spikes (+4 = 64 tools).
"""
import asyncio
@@ -635,6 +636,51 @@ TOOLS: list[Tool] = [
},
},
),
# --- NLP Intelligence (4 tools) ---
Tool(
name="intel_extract_entities",
description="Extract named entities (countries, leaders, organizations, companies, CVEs, APT groups) from text or recent news headlines.",
inputSchema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to analyze. If omitted, analyzes recent news headlines."},
},
},
),
Tool(
name="intel_classify_event",
description="Classify text into threat categories (military, terrorism, cyber, political, economic, health, climate, nuclear, etc.) with severity scoring (1-10).",
inputSchema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Event text or headline to classify."},
},
"required": ["text"],
},
),
Tool(
name="intel_news_clusters",
description="Cluster recent news articles by topic similarity using Jaccard coefficient. Groups related stories and extracts top keywords per cluster.",
inputSchema={
"type": "object",
"properties": {
"category": {"type": "string", "description": "RSS feed category filter (geopolitics, security, military, etc.)"},
"limit": {"type": "integer", "description": "Max news items to cluster (default: 100)"},
"threshold": {"type": "number", "description": "Similarity threshold 0.0-1.0 (default: 0.25)"},
},
},
),
Tool(
name="intel_keyword_spikes",
description="Detect trending keyword spikes against historical baselines using Welford's algorithm. Extracts CVE identifiers and APT group mentions.",
inputSchema={
"type": "object",
"properties": {
"min_count": {"type": "integer", "description": "Minimum keyword frequency to consider (default: 3)"},
"z_threshold": {"type": "number", "description": "Z-score threshold for spike detection (default: 2.0)"},
},
},
),
# --- System (1 tool) ---
Tool(
name="intel_status",
@@ -902,6 +948,29 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
status=arguments.get("status"),
)
# NLP Intelligence
case "intel_extract_entities":
from .analysis.entities import fetch_entity_extraction
return await fetch_entity_extraction(fetcher, text=arguments.get("text"))
case "intel_classify_event":
from .analysis.classifier import fetch_classify_event
return await fetch_classify_event(fetcher, text=arguments["text"])
case "intel_news_clusters":
from .analysis.clustering import fetch_news_clusters
return await fetch_news_clusters(
fetcher,
category=arguments.get("category"),
limit=arguments.get("limit", 100),
threshold=arguments.get("threshold", 0.25),
)
case "intel_keyword_spikes":
from .analysis.spikes import fetch_keyword_spikes
return await fetch_keyword_spikes(
fetcher,
min_count=arguments.get("min_count", 3),
z_threshold=arguments.get("z_threshold", 2.0),
)
# System
case "intel_status":
return {
@@ -932,6 +1001,7 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
"nuclear": ["usgs-nuclear-monitor"],
"service_status": ["aws", "azure", "gcp", "cloudflare", "github"],
"geospatial": ["static-datasets (bases, ports, pipelines, nuclear)"],
"nlp": ["regex-ner", "keyword-classifier", "jaccard-clustering", "keyword-spike-detector"],
},
}