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,132 @@
|
||||
"""Read existing project-owned peripheral facts without forwarding MCP tokens."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException
|
||||
from app.infra.db.postgresql.read_queries import MAX_QUERY_BYTES, read_rows, scada_devices, series_rows
|
||||
from app.domain.schemas.operational_data import TaskDataSnapshot, TaskDataQuery
|
||||
from app.services.content_digest import digest
|
||||
from samples.peripheral.store import PostgresStore
|
||||
from app.infra.integrations.operational_data import _select_demo_observations
|
||||
|
||||
|
||||
async def read_peripheral_query(scope, args):
|
||||
conn = scope.connection
|
||||
project = str(scope.project.project_id)
|
||||
truncated = False
|
||||
origin = "system_data"
|
||||
if args.source in {"peripheral_work_orders", "peripheral_test_data"}:
|
||||
table, column = (
|
||||
("work_orders", "work_order_id")
|
||||
if args.source == "peripheral_work_orders"
|
||||
else ("test_data_records", "record_id")
|
||||
)
|
||||
rows = await read_rows(
|
||||
conn,
|
||||
"SELECT document FROM peripheral_sample."
|
||||
+ table
|
||||
+ " WHERE project_id=%s AND deleted_at IS NULL ORDER BY "
|
||||
+ column
|
||||
+ " LIMIT %s OFFSET %s",
|
||||
(project, args.limit + 1, args.offset),
|
||||
)
|
||||
items = [
|
||||
PostgresStore._active_managed_document(row["document"])
|
||||
for row in rows[: args.limit]
|
||||
]
|
||||
truncated = len(rows) > args.limit
|
||||
if any(item.get("project_id") != project for item in items):
|
||||
raise HTTPException(502, "Peripheral project mismatch")
|
||||
data = {
|
||||
"items": items,
|
||||
"truncated": truncated,
|
||||
"limit": args.limit,
|
||||
"offset": args.offset,
|
||||
}
|
||||
origin = (
|
||||
"synthetic"
|
||||
if items and any(item.get("origin") == "synthetic" for item in items)
|
||||
else "system_data"
|
||||
)
|
||||
elif args.source == "peripheral_points":
|
||||
rows = await scada_devices(conn, limit=args.limit + 1, offset=args.offset)
|
||||
data = {
|
||||
"origin": "historical_replay",
|
||||
"items": [
|
||||
{
|
||||
"point_id": row["device_id"],
|
||||
"label": row["device_id"] + " · " + row["device_type"],
|
||||
"unit": row["measurement_unit"],
|
||||
"status": "unknown",
|
||||
"origin": "historical_replay",
|
||||
}
|
||||
for row in rows[: args.limit]
|
||||
],
|
||||
"truncated": len(rows) > args.limit,
|
||||
}
|
||||
truncated = data["truncated"]
|
||||
elif args.point_id.startswith("DEMO-"):
|
||||
rows = await read_rows(
|
||||
conn,
|
||||
"SELECT CASE WHEN octet_length(document::text)<=%s THEN document END AS document,"
|
||||
"digest,octet_length(document::text)>%s AS too_large "
|
||||
"FROM peripheral_sample.demo_datasets WHERE project_id=%s AND dataset_id='water-demo-v1'",
|
||||
(MAX_QUERY_BYTES, MAX_QUERY_BYTES, project),
|
||||
)
|
||||
if rows and rows[0]["too_large"]:
|
||||
raise HTTPException(413, "Synthetic dataset exceeds the integrity-check byte budget")
|
||||
if not rows or digest(rows[0]["document"]) != rows[0]["digest"]:
|
||||
raise HTTPException(503, "Synthetic dataset is unavailable or invalid")
|
||||
query = TaskDataQuery(
|
||||
source=args.source, point_id=args.point_id, start=args.start, end=args.end
|
||||
)
|
||||
data = _select_demo_observations(
|
||||
{**rows[0]["document"], "project_id": project}, query, project
|
||||
)
|
||||
items = data["items"]
|
||||
truncated = len(items) > args.limit + args.offset
|
||||
data["items"] = items[args.offset : args.offset + args.limit]
|
||||
data["truncated"] = truncated
|
||||
origin = "synthetic"
|
||||
else:
|
||||
devices = await scada_devices(conn, device_id=args.point_id, limit=1)
|
||||
if not devices:
|
||||
raise HTTPException(404, "Peripheral point was not found")
|
||||
device = devices[0]
|
||||
if not device.get("api_query_id"):
|
||||
raise HTTPException(503, "Peripheral history mapping is unavailable")
|
||||
async with scope.timescale() as ts:
|
||||
rows = await series_rows(
|
||||
ts,
|
||||
source="scada",
|
||||
element_type="junction",
|
||||
ids=[device["api_query_id"]],
|
||||
start=args.start,
|
||||
end=args.end,
|
||||
field="monitored_value",
|
||||
limit=args.limit + 1,
|
||||
offset=args.offset,
|
||||
end_exclusive=True,
|
||||
)
|
||||
truncated = len(rows) > args.limit
|
||||
data = {
|
||||
"origin": "historical_replay",
|
||||
"items": [
|
||||
{
|
||||
"point_id": args.point_id,
|
||||
"observed_at": row["time"].isoformat(),
|
||||
"value": row["value"],
|
||||
"unit": device["measurement_unit"],
|
||||
}
|
||||
for row in rows[: args.limit]
|
||||
],
|
||||
"truncated": truncated,
|
||||
}
|
||||
return TaskDataSnapshot(
|
||||
source=args.source,
|
||||
source_ref="mcp:" + str(uuid4()),
|
||||
queried_at=datetime.now(timezone.utc),
|
||||
data=data,
|
||||
origin=origin,
|
||||
truncated=truncated,
|
||||
)
|
||||
Reference in New Issue
Block a user