mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 09:24:44 +02:00
feat(v0.6-5): env_reporter + router wiring + include_in_llm_context per-adapter toggle -- LLM gains read access to every adapter table via the existing mesh_reporter pre-rendered prompt-injection pattern
Closes audit doc Section C. The LLM can now answer "any fires near me?",
"how are band conditions?", "why didnt I hear about that quake?"
without any tool-use / MCP / SQL pass-through -- via the same prompt-
injection contract mesh_reporter uses.
env_reporter (meshai/notifications/env_reporter.py):
- EnvReporter class with build_env_summary / build_fires_detail /
build_alerts_detail / build_quakes_detail / build_traffic_detail /
build_gauges_detail / build_swpc_detail / build_drop_audit / build_all
- Reads from fires + firms_pixels + nws_alerts + quake_events +
traffic_events + gauge_readings + swpc_events +
band_conditions_broadcasts + event_log + dispatcher_state
- Each build_*_detail() checks adapter_meta.include_in_llm_context for
the relevant adapter(s) before reading; turning the meta off via
/api/adapter-meta drops that adapters block out of the LLM prompt
- Defensive: missing meta row defaults to True (include); DB-unavailable
returns empty string; per-block 3000-char cap
- Module-level env_reporter singleton for the router
Router wiring (meshai/router.py):
- Extended _MESH_KEYWORDS dispatcher with _ENV_KEYWORDS_TO_SUBTYPE
mapping (fire/quake/flood/warning/storm/road/swpc/etc -> coarse
subtype). "flood" intentionally precedes "warning" so
"river flood warning" routes to gauges, not alerts
- _detect_env_subtype helper at module level (also test-importable)
- _is_mesh_question now also fires for env keywords -- single detector
per Matt s spec
- _detect_mesh_scope returns ("env", subtype) when an env keyword
matches, taking precedence over the node/region branches
- generate_llm_response: when scope_type == "env", appends
env_reporter.build_all() + env_reporter.build_drop_audit(hours=1)
to the system prompt. Wrapped in try/except so a reporter fault
never blocks the LLM call
Tests:
- tests/test_env_reporter.py (18 cases): meta gate, every build_*
method shape, build_all combines blocks, all-off produces empty
- tests/test_router_env_scope.py (18 cases): parametrized subtype
detection across fires/quakes/alerts/gauges/traffic/swpc, word-
boundary check (firearm != fire), synthetic-probe end-to-end
(seed fires table -> env_reporter emits a fires block with the
seeded row)
Test count: 761 -> 797 (+36 new, 0 regressions).
This commit is contained in:
parent
42b3106e97
commit
eb84f27941
4 changed files with 906 additions and 16 deletions
|
|
@ -65,6 +65,50 @@ _MESH_KEYWORDS = {
|
|||
"repeaters", "regions", "localities", "score", "status",
|
||||
}
|
||||
|
||||
# v0.6-5: env keywords expand the mesh-question detector so the LLM gets
|
||||
# env_reporter blocks when the user asks about fires/quakes/weather/etc.
|
||||
# Each keyword maps to a coarse subtype used by _detect_env_subtype.
|
||||
_ENV_KEYWORDS_TO_SUBTYPE: dict[str, str] = {
|
||||
# fires
|
||||
"fire": "fires", "fires": "fires", "wildfire": "fires",
|
||||
"wildfires": "fires", "hotspot": "fires", "hotspots": "fires",
|
||||
"burning": "fires", "smoke": "fires",
|
||||
# quakes
|
||||
"quake": "quakes", "quakes": "quakes", "earthquake": "quakes",
|
||||
"earthquakes": "quakes", "seismic": "quakes", "tsunami": "quakes",
|
||||
# gauges (placed BEFORE weather alerts so "flood" wins over "warning"
|
||||
# in cases like "river flood warning")
|
||||
"flood": "gauges", "flooding": "gauges",
|
||||
"gauge": "gauges", "river": "gauges", "stream": "gauges",
|
||||
# weather alerts
|
||||
"warning": "alerts", "watch": "alerts", "advisory": "alerts",
|
||||
"tornado": "alerts", "thunderstorm": "alerts", "blizzard": "alerts",
|
||||
# space weather + band conditions
|
||||
"swpc": "swpc", "geomag": "swpc", "solar": "swpc", "kp": "swpc",
|
||||
"propagation": "swpc", "aurora": "swpc",
|
||||
"band": "swpc", "bands": "swpc", "hf": "swpc",
|
||||
# traffic / roads
|
||||
"road": "traffic", "roads": "traffic", "jam": "traffic",
|
||||
"crash": "traffic", "closure": "traffic", "511": "traffic",
|
||||
"incident": "traffic", "incidents": "traffic",
|
||||
# generic
|
||||
"storm": "alerts", "weather": "alerts",
|
||||
}
|
||||
|
||||
|
||||
def _detect_env_subtype(message_lower: str) -> Optional[str]:
|
||||
"""Return the env subtype matched by the first env keyword in the message.
|
||||
|
||||
`None` when no env keyword matches. Uses set intersection on tokenized
|
||||
words so partial-word collisions (e.g. "firearm" / "fire") don\'t fire."""
|
||||
if not message_lower:
|
||||
return None
|
||||
words = set(re.findall(r"\b\w+\b", message_lower))
|
||||
for kw, subtype in _ENV_KEYWORDS_TO_SUBTYPE.items():
|
||||
if kw in words:
|
||||
return subtype
|
||||
return None
|
||||
|
||||
# Phrases that indicate mesh questions
|
||||
_MESH_PHRASES = [
|
||||
"how's the mesh",
|
||||
|
|
@ -333,42 +377,44 @@ class MessageRouter:
|
|||
return RouteResult(RouteType.LLM, query=query)
|
||||
|
||||
def _is_mesh_question(self, message: str) -> bool:
|
||||
"""Check if message is asking about mesh health/status.
|
||||
"""Check if message is asking about mesh health/status OR env state.
|
||||
|
||||
Args:
|
||||
message: User message text
|
||||
|
||||
Returns:
|
||||
True if this is a mesh-related question
|
||||
v0.6-5: env keywords (fire/quake/flood/etc.) also trigger the
|
||||
mesh-question path so the env_reporter blocks land in the system
|
||||
prompt. Single detector per Matt\'s spec.
|
||||
"""
|
||||
msg_lower = message.lower()
|
||||
|
||||
# Check for mesh phrases
|
||||
# Mesh phrases.
|
||||
for phrase in _MESH_PHRASES:
|
||||
if phrase in msg_lower:
|
||||
return True
|
||||
|
||||
# Check for mesh keywords
|
||||
# Mesh keywords + env keywords.
|
||||
words = set(re.findall(r'\b\w+\b', msg_lower))
|
||||
if words & _MESH_KEYWORDS:
|
||||
return True
|
||||
if _detect_env_subtype(msg_lower) is not None:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _detect_mesh_scope(self, message: str) -> tuple[str, Optional[str]]:
|
||||
"""Detect the scope of a mesh question.
|
||||
|
||||
Args:
|
||||
message: User message text
|
||||
|
||||
Returns:
|
||||
Tuple of (scope_type, scope_value):
|
||||
- ("node", "{identifier}") if asking about specific node
|
||||
- ("region", "{region_name}") if asking about specific region
|
||||
- ("mesh", None) for general mesh questions
|
||||
Returns one of:
|
||||
- ("env", subtype) : fires/quakes/alerts/gauges/traffic/swpc
|
||||
- ("node", id) : specific node
|
||||
- ("region", name) : specific region
|
||||
- ("mesh", None) : general mesh question
|
||||
"""
|
||||
msg_lower = message.lower()
|
||||
|
||||
# === ENV (v0.6-5: check first; env scope routes through env_reporter) ===
|
||||
env_subtype = _detect_env_subtype(msg_lower)
|
||||
if env_subtype is not None:
|
||||
return ("env", env_subtype)
|
||||
|
||||
# === NODE MATCHING (check first - more specific) ===
|
||||
if self.health_engine and self.health_engine.mesh_health:
|
||||
health = self.health_engine.mesh_health
|
||||
|
|
@ -693,6 +739,23 @@ class MessageRouter:
|
|||
|
||||
should_inject_mesh = is_direct_mesh_question or is_followup
|
||||
|
||||
# v0.6-5 env_reporter: when scope is "env" OR when injecting mesh
|
||||
# context, append the env_reporter blocks. The reporter itself gates
|
||||
# per-adapter via adapter_meta.include_in_llm_context.
|
||||
if should_inject_mesh and scope_type == "env":
|
||||
try:
|
||||
from meshai.notifications.env_reporter import env_reporter
|
||||
env_block = env_reporter.build_all()
|
||||
if env_block:
|
||||
system_prompt += "\n\n" + env_block
|
||||
# Drop audit is useful for "why didn\'t I hear about X?" --
|
||||
# always include the most-recent hour when env scope.
|
||||
drop_block = env_reporter.build_drop_audit(hours=1)
|
||||
if drop_block:
|
||||
system_prompt += "\n\n" + drop_block
|
||||
except Exception:
|
||||
logger.exception("env_reporter injection failed")
|
||||
|
||||
if self.source_manager and self.mesh_reporter and should_inject_mesh:
|
||||
# Detect scope from current message
|
||||
scope_type, scope_value = self._detect_mesh_scope(query)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue