Files
next-tjwater-server/app/services/operational_presentation.py
jiang be6317eb48 feat(server): integrate MCP queries and investigation messaging
Add project-bound OAuth MCP queries and controlled analysis with scoped evidence,
limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs.
Default nearby searches to 200 meters and five results.

Integrate investigation messaging/presentation, health/readiness and typed history;
remove duplicate web search and fix online simulation and compressed baseline writes.
Include project migrations 023-025; keep local credentials and demo seeding excluded.

Validation: 319 focused pytest checks; real PostGIS temporary-table regression;
read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
2026-10-19 18:46:58 +08:00

100 lines
5.2 KiB
Python

"""Resolve browser overlays only from immutable Server-ingested snapshots."""
from __future__ import annotations
from math import isfinite
from app.domain.domain_errors import require
from app.domain.schemas.operational_data import TaskDataSnapshot
def _number(value) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value)
def _position(value):
require(isinstance(value, list) and len(value) in {2, 3}
and all(_number(item) for item in value)
and -180 <= value[0] <= 180 and -90 <= value[1] <= 90,
"OVERLAY_GEOMETRY_INVALID", "Overlay coordinates must be finite WGS84 positions", status=422)
return value[:2]
def _geometry(value):
require(isinstance(value, dict), "OVERLAY_GEOMETRY_INVALID", "Overlay geometry is missing", status=422)
kind = value.get("type")
depths = {"Point": 0, "MultiPoint": 1, "LineString": 1,
"MultiLineString": 2, "Polygon": 2, "MultiPolygon": 3}
require(kind in depths, "OVERLAY_GEOMETRY_INVALID", "Unsupported overlay geometry", status=422)
positions = 0
def coordinates(items, depth):
nonlocal positions
if depth == 0:
positions += 1
require(positions <= 20000, "OVERLAY_TOO_LARGE", "Overlay geometry is too large", status=422)
return _position(items)
require(isinstance(items, list) and 0 < len(items) <= 20000,
"OVERLAY_GEOMETRY_INVALID", "Overlay coordinates are empty or too large", status=422)
return [coordinates(item, depth - 1) for item in items]
normalized = coordinates(value.get("coordinates"), depths[kind])
lines = ([normalized] if kind == "LineString" else normalized if kind == "MultiLineString" else [])
require(all(len(line) >= 2 for line in lines), "OVERLAY_GEOMETRY_INVALID",
"Overlay lines need at least two positions", status=422)
rings = (normalized if kind == "Polygon" else [ring for polygon in normalized for ring in polygon]
if kind == "MultiPolygon" else [])
require(all(len(ring) >= 4 and ring[0] == ring[-1] for ring in rings),
"OVERLAY_GEOMETRY_INVALID", "Overlay polygon rings must be closed", status=422)
return {"type": kind, "coordinates": normalized}
def resolve_overlay(snapshot: TaskDataSnapshot, mode: str) -> dict:
"""Use recorded GeoJSON or recorded coordinates; never geocode or infer a boundary."""
data = snapshot.data
candidates = [data]
if isinstance(data, dict):
candidates.extend([data.get("map_payload"), data.get("drawing_payload")])
latest = data.get("latest_analysis")
if isinstance(latest, dict):
candidates.append(latest.get("map_payload"))
collection = next((item for item in candidates
if isinstance(item, dict) and item.get("type") == "FeatureCollection"), None)
if collection is not None:
raw_features = collection.get("features")
require(isinstance(raw_features, list), "OVERLAY_GEOMETRY_INVALID",
"Snapshot FeatureCollection is invalid", status=422)
else:
rows = data.get("items") if isinstance(data, dict) else data
require(isinstance(rows, list), "OVERLAY_DATA_MISSING",
"Evidence snapshot has no recorded coordinates or GeoJSON", status=422)
raw_features = []
for row in rows:
if not isinstance(row, dict) or row.get("longitude") is None or row.get("latitude") is None:
continue
raw_features.append({
"type": "Feature", "geometry": {"type": "Point", "coordinates": [row["longitude"], row["latitude"]]},
"id": row.get("device_id") or row.get("point_id") or row.get("work_order_id") or row.get("id"),
"properties": {"weight": row.get("weight", 1)},
})
require(0 < len(raw_features) <= 1000, "OVERLAY_DATA_MISSING",
"Overlay needs between one and 1000 recorded features", status=422)
features = []
for feature in raw_features:
require(isinstance(feature, dict) and feature.get("type") == "Feature",
"OVERLAY_GEOMETRY_INVALID", "Snapshot contains an invalid feature", status=422)
geometry = _geometry(feature.get("geometry"))
require(mode != "heatmap" or geometry["type"] == "Point", "OVERLAY_HEATMAP_POINTS_REQUIRED",
"Heatmaps require recorded points, not inferred polygon centroids", status=422)
properties = feature.get("properties") or {}
require(isinstance(properties, dict), "OVERLAY_GEOMETRY_INVALID", "Feature properties are invalid", status=422)
normalized = {"type": "Feature", "geometry": geometry, "properties": dict(properties)}
if isinstance(feature.get("id"), (str, int)) and not isinstance(feature.get("id"), bool):
normalized["id"] = feature["id"]
if mode == "heatmap":
weight = properties.get("weight", 1)
require(_number(weight) and weight >= 0, "OVERLAY_WEIGHT_INVALID",
"Heatmap weight must be a finite nonnegative number", status=422)
normalized["properties"]["weight"] = weight
features.append(normalized)
return {"type": "FeatureCollection", "features": features}