feat: add node coverage and improve investigation presentation
This commit is contained in:
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Annotated, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from app.mcp.models import Id, StrictInput
|
||||
|
||||
@@ -19,6 +19,36 @@ class CalculationInput(StrictInput):
|
||||
)
|
||||
|
||||
|
||||
class NodeCoverageGroup(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
group_id: Id = Field(max_length=80)
|
||||
title: str = Field(min_length=1, max_length=200)
|
||||
node_ids: list[Id] = Field(min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class NodeCoverageInput(CalculationInput):
|
||||
groups: list[NodeCoverageGroup] = Field(min_length=1, max_length=8,
|
||||
description="已核实的模型节点集合;各组为需要比较的业务或空间集合,可重叠,不接受任意坐标")
|
||||
concave_ratio: float = Field(default=0.25, ge=0, le=1, allow_inf_nan=False,
|
||||
description="凹包比例;越接近1越平滑接近凸包,参数只决定示意形状")
|
||||
buffer_m: float = Field(default=25, gt=0, le=1000, allow_inf_nan=False,
|
||||
description="展示缓冲距离,米;不是服务半径或影响距离")
|
||||
split_by_connectivity: bool = Field(default=False, strict=True,
|
||||
description="需要按模型连接分组时设true;地理分布比较默认false,不能据此认定实际供水关系")
|
||||
excluded_link_ids: list[Id] = Field(default_factory=list, max_length=200,
|
||||
description="连通分组时额外视为断开的管段/阀门,例如已有隔离建议中的关阀ID;仅影响绘图分组")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def bounded_groups(self):
|
||||
if len({group.group_id for group in self.groups}) != len(self.groups):
|
||||
raise ValueError("coverage group IDs must be distinct")
|
||||
if len({node for group in self.groups for node in group.node_ids}) > 10000:
|
||||
raise ValueError("coverage supports at most 10000 distinct nodes")
|
||||
if self.excluded_link_ids and not self.split_by_connectivity:
|
||||
raise ValueError("excluded_link_ids requires split_by_connectivity")
|
||||
return self
|
||||
|
||||
|
||||
class BurstDetectionInput(CalculationInput):
|
||||
target_time: datetime = Field(description="带时区的侦测时刻;读取此前15天同周期压力数据,不是单小时定位")
|
||||
sensor_nodes: list[Id] = Field(min_length=5, max_length=20, description="已核实的压力监测节点ID,至少5个完整传感器")
|
||||
|
||||
@@ -71,6 +71,10 @@ def _calculate(name: str, parameters: dict[str, Any], network: str, username: st
|
||||
from app.services.valve_isolation import analyze_valve_isolation
|
||||
|
||||
return analyze_valve_isolation(network=network, **parameters)
|
||||
if name == "generate_node_coverage":
|
||||
from app.services.node_coverage import generate_node_coverage
|
||||
|
||||
return generate_node_coverage(network=network, **parameters)
|
||||
raise ValueError("Calculation is not registered")
|
||||
finally:
|
||||
_slots.release()
|
||||
|
||||
@@ -441,6 +441,10 @@ class McpQueryApplication:
|
||||
"resource_uri": uri,
|
||||
"preview": "The complete query snapshot is saved in task evidence",
|
||||
}
|
||||
from app.mcp.presentation_preview import saved_presentation_preview
|
||||
presentation = saved_presentation_preview(result)
|
||||
if presentation is not None:
|
||||
output["data"]["presentation"] = presentation
|
||||
output["truncated"] = True
|
||||
output["data_gaps"] = [
|
||||
*result.data_gaps,
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ from mcp import types
|
||||
from app.mcp import models as m
|
||||
from app.mcp import calculation_models as jobs
|
||||
from app.mcp import extended_models as e
|
||||
from app.mcp.analysis_models import BurstDetectionInput, BurstLocationInput, ValveIsolationInput, LeakageIdentificationInput
|
||||
from app.mcp.analysis_models import BurstDetectionInput, BurstLocationInput, ValveIsolationInput, LeakageIdentificationInput, NodeCoverageInput
|
||||
from app.mcp.analysis_models import ScenarioInput, ContaminantSimulationInput, ValveClosureInput, FlushingInput, PressureRegulationInput, SensorPlacementInput
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ _specs = [
|
||||
("run_burst_detection", "爆管异常侦测:使用至少5个完整压力传感器此前15天数据;只提示压力异常,不定位、不证明爆管", BurstDetectionInput, "burst.run", "analysis", False, True),
|
||||
("run_burst_location", "爆管候选定位:对比异常与正常压力,可辅助流量;burst_leakage必需,可使用有依据的估计或明确给定的情景假设,不要求现场确诊;本工具不反演总漏水量。压力节点可省略,自动读取项目SCADA压力测点;monitoring正常窗可成对省略,默认前一天同时间段,须说明该对照假设。simulation须有已有分析运行UUID并读取同窗模型基线,不是实测;定位内部会执行水力计算,不创建新的事件方案", BurstLocationInput, "burst.run", "analysis", False, True),
|
||||
("analyze_valve_isolation", "按事故候选要素分析拓扑隔离边界、建议关闭阀门和受影响节点;候选分析无需等待现场确诊。输出不是污染扩散或水力影响范围;现场操作前须核实阀门映射及可用性,本工具不执行关阀", ValveIsolationInput, "simulation.run", "analysis", False, True),
|
||||
("generate_node_coverage", "为当前项目已核实的模型节点集合生成空间覆盖示意,适用于候选分布、监测覆盖比较、区域成员、隔离或模拟结果节点等,不限调查任务。支持最多8组、合计10000个不同节点,从真实GIS查询坐标;监测设备、投诉或地址须先关联模型节点。可按连接分组,记录凹包/缓冲参数、缺失节点和display_only;不计算故障、污染、实际停水或服务边界。返回result.map_payload及coverage,任务中用set_overlay引用本次evidence_id、省略data展示;生成不自动操作地图", NodeCoverageInput, "webgis.view", "analysis", False, True),
|
||||
(
|
||||
"get_element_properties",
|
||||
"读取当前项目的单个管网元素属性",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Keep a saved geometry discoverable even when its inline response is shortened."""
|
||||
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
from app.mcp.models import QueryResult
|
||||
|
||||
|
||||
def saved_presentation_preview(result: QueryResult) -> dict | None:
|
||||
if result.source != "generate_node_coverage" or not result.evidence_id:
|
||||
return None
|
||||
analysis = result.data.get("result") if isinstance(result.data, dict) else None
|
||||
if not isinstance(analysis, dict):
|
||||
return None
|
||||
payload = analysis.get("map_payload")
|
||||
if not isinstance(payload, dict) or not payload.get("features"):
|
||||
return None
|
||||
coverage = analysis.get("coverage", {})
|
||||
return {
|
||||
"command": {
|
||||
"action": "set_overlay", "mode": "geometry", "evidence_id": result.evidence_id,
|
||||
"overlay_id": f"node-coverage-{uuid5(NAMESPACE_URL, result.evidence_id)}",
|
||||
"title": "节点空间覆盖示意",
|
||||
},
|
||||
"display_only": True,
|
||||
"coverage_status": coverage.get("status"),
|
||||
"groups": {role: {
|
||||
"node_count": len(group.get("node_ids", [])),
|
||||
"mapped_node_count": group.get("mapped_node_count", 0),
|
||||
"missing_node_count": len(group.get("missing_node_ids", [])),
|
||||
} for role, group in coverage.get("groups", {}).items()},
|
||||
}
|
||||
@@ -179,6 +179,7 @@ async def execute_query(
|
||||
"run_burst_detection": "Pressure anomalies alone do not confirm a burst or locate a pipe.",
|
||||
"run_burst_location": "Candidate ranking depends on the leakage assumption, model and sensor coverage; field confirmation is required.",
|
||||
"analyze_valve_isolation": "Topology-only isolation advice; valve operability, hydraulic impact and field actions are not verified or executed.",
|
||||
"generate_node_coverage": "Derived node coverage for display only; geometry does not establish a fault, contamination, outage or service boundary. Missing GIS positions are reported separately.",
|
||||
"calculate_demand_allocation": "Pipe-length-weighted allocation only; model demands were not modified. Units follow the current model.",
|
||||
"run_burst_simulation": "Forward hydraulic simulation for assumed orifice areas, not detection or confirmed burst location; only requested elements returned.",
|
||||
}[name]]
|
||||
|
||||
Reference in New Issue
Block a user