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.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""Bounded nearest-model queries on the already authorized project connection."""
|
||||
|
||||
from math import asin, cos, degrees, radians, sin
|
||||
from typing import Any
|
||||
|
||||
from psycopg import AsyncConnection, sql
|
||||
|
||||
from app.infra.db.postgresql.read_queries import ELEMENT_VIEWS, read_rows
|
||||
|
||||
_MODEL_RELATIONS = {
|
||||
kind: sql.Identifier("gis", view) for kind, view in ELEMENT_VIEWS.items()
|
||||
}
|
||||
|
||||
|
||||
def _search_boxes(
|
||||
longitude: float, latitude: float, radius_m: float,
|
||||
) -> list[tuple[float, float, float, float]]:
|
||||
# A deliberately smaller-than-WGS84 curvature radius gives a conservative
|
||||
# geographic envelope. It is only a GiST candidate filter; geography below
|
||||
# determines the actual radius and distances. Split at the antimeridian.
|
||||
angle = (radius_m + 10) / 6_300_000
|
||||
low, high = latitude - degrees(angle), latitude + degrees(angle)
|
||||
if low < -85 or high > 85:
|
||||
# Avoid Mercator's polar singularity; retain exact geography filtering.
|
||||
return []
|
||||
width = degrees(asin(min(1.0, sin(angle) / cos(radians(latitude)))))
|
||||
left, right = longitude - width, longitude + width
|
||||
if left < -180:
|
||||
return [(-180, low, right, high), (left + 360, low, 180, high)]
|
||||
if right > 180:
|
||||
return [(left, low, 180, high), (-180, low, right - 360, high)]
|
||||
return [(left, low, right, high)]
|
||||
|
||||
|
||||
async def nearby_elements(
|
||||
conn: AsyncConnection,
|
||||
element_type: str,
|
||||
*,
|
||||
center: tuple[float, float],
|
||||
radius_m: float,
|
||||
limit: int,
|
||||
offset: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Rank complete model geometries by spheroidal distance, never endpoints."""
|
||||
relation = _MODEL_RELATIONS[element_type]
|
||||
filters = []
|
||||
parameters: list[Any] = [*center]
|
||||
for box in _search_boxes(*center, radius_m):
|
||||
filters.append(sql.SQL(
|
||||
"v.geom && ST_Transform(ST_MakeEnvelope(%s,%s,%s,%s,4326),3857)"
|
||||
))
|
||||
parameters.extend(box)
|
||||
candidate_filter = sql.SQL(" OR ").join(filters) if filters else sql.SQL("TRUE")
|
||||
statement = sql.SQL("""
|
||||
WITH target AS (
|
||||
SELECT ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography AS point
|
||||
), candidates AS MATERIALIZED (
|
||||
SELECT v.id, to_jsonb(v)-'geom' AS properties,
|
||||
ST_Transform(v.geom,4326)::geography AS shape
|
||||
FROM {} v
|
||||
WHERE v.geom IS NOT NULL AND NOT ST_IsEmpty(v.geom) AND ({})
|
||||
)
|
||||
SELECT c.id AS element_id, c.properties,
|
||||
ST_Distance(c.shape,t.point,true) AS distance_m
|
||||
FROM candidates c CROSS JOIN target t
|
||||
WHERE ST_DWithin(c.shape,t.point,%s,true)
|
||||
ORDER BY distance_m,c.id
|
||||
LIMIT %s OFFSET %s
|
||||
""").format(relation, candidate_filter)
|
||||
parameters.extend((radius_m, limit, offset))
|
||||
return await read_rows(conn, statement, tuple(parameters))
|
||||
Reference in New Issue
Block a user