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:
@@ -202,11 +202,24 @@ class InvestigationDataGap(StrictModel):
|
||||
blocking: StrictBool
|
||||
|
||||
|
||||
TaskMapLayer = Literal["junctions", "pipes", "valves", "pumps", "tanks", "reservoirs", "scada"]
|
||||
TaskFeatureId = Annotated[str, Field(min_length=1, max_length=128)]
|
||||
|
||||
|
||||
class InvestigationMapTarget(StrictModel):
|
||||
source_id: TaskMapLayer
|
||||
feature_id: TaskFeatureId
|
||||
|
||||
|
||||
class InvestigationDiagramNodeRef(StrictModel):
|
||||
node_id: Annotated[str, Field(pattern=r"^[A-Za-z_][A-Za-z0-9_-]{0,79}$")]
|
||||
label: NonEmptyStr
|
||||
evidence_refs: list[NonEmptyStr] = Field(default_factory=list)
|
||||
action_refs: list[UUID] = Field(default_factory=list)
|
||||
status: Literal[
|
||||
"pending", "running", "completed", "waiting_human", "failed", "skipped", "cancelled"
|
||||
] | None = None
|
||||
map_targets: list[InvestigationMapTarget] = Field(default_factory=list, max_length=100)
|
||||
|
||||
|
||||
class InvestigationDiagram(StrictModel):
|
||||
@@ -271,6 +284,22 @@ class InvestigationReportInput(InvestigationReport):
|
||||
data_gaps: list[InvestigationDataGap]
|
||||
|
||||
|
||||
class InvestigationProgress(StrictModel):
|
||||
"""A complete reading snapshot, not a plan or a business result."""
|
||||
|
||||
summary: Annotated[str, Field(min_length=1, max_length=20000)]
|
||||
diagram: InvestigationDiagram | None = None
|
||||
|
||||
|
||||
class InvestigationProgressSnapshot(StrictModel):
|
||||
revision: PositiveInt
|
||||
run_id: UUID
|
||||
execution_generation: PositiveInt
|
||||
published_at: WorkbenchDateTime
|
||||
source_session_id: NonEmptyStr
|
||||
progress: InvestigationProgress
|
||||
|
||||
|
||||
class ExecutionPlanSnapshot(StrictModel):
|
||||
revision: PositiveInt
|
||||
execution_generation: PositiveInt | None = None
|
||||
@@ -306,10 +335,11 @@ class AgentArtifactsCommand(StrictModel):
|
||||
evidence: list[EvidenceInput] = Field(default_factory=list, max_length=100)
|
||||
plan: ExecutionPlanInput | None = None
|
||||
report: InvestigationReportInput | None = None
|
||||
progress: InvestigationProgress | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def require_artifact(self):
|
||||
if not self.evidence and self.plan is None and self.report is None:
|
||||
if not self.evidence and self.plan is None and self.report is None and self.progress is None:
|
||||
raise ValueError("at least one artifact must be supplied")
|
||||
return self
|
||||
|
||||
@@ -507,6 +537,7 @@ class OperationalTaskSummary(StrictModel):
|
||||
workflow: NonEmptyStr
|
||||
lifecycle_status: TaskLifecycleStatus = "open"
|
||||
active_run: OperationalTaskRun | None = None
|
||||
latest_run: OperationalTaskRun | None = None
|
||||
# Compatibility fields for the staged 014 -> 015 rollout. New consumers
|
||||
# must use lifecycle_status and active_run.
|
||||
status: TaskStatus
|
||||
@@ -541,6 +572,7 @@ class OperationalTaskPermissions(StrictModel):
|
||||
class OperationalTaskDetail(OperationalTaskSummary):
|
||||
latest_plan: ExecutionPlanSnapshot | None
|
||||
latest_report: InvestigationReportRevision | None
|
||||
latest_progress: InvestigationProgressSnapshot | None = None
|
||||
evidence: list[EvidenceRecord]
|
||||
actions: list[HumanApprovalAction]
|
||||
access_requests: list[TaskAccessRequest] = Field(default_factory=list)
|
||||
@@ -598,6 +630,8 @@ class OperationalTaskRetryCommand(StrictModel):
|
||||
|
||||
class RecoverExpiredLeasesCommand(StrictModel):
|
||||
limit: int = Field(default=20, ge=1, le=100)
|
||||
task_type: TaskType | None = None
|
||||
task_created_from: WorkbenchDateTime | None = None
|
||||
|
||||
|
||||
class RecoverExpiredLeasesResult(StrictModel):
|
||||
@@ -609,6 +643,94 @@ class AgentArtifactsResult(StrictModel):
|
||||
task: OperationalTaskDetail
|
||||
plan_revision: PositiveInt | None
|
||||
report_revision: PositiveInt | None
|
||||
progress_revision: PositiveInt | None = None
|
||||
|
||||
|
||||
class TaskSafeChartSeries(StrictModel):
|
||||
name: Annotated[str, Field(max_length=128)]
|
||||
data: Annotated[list[Annotated[float, Field(strict=True, allow_inf_nan=False)]], Field(min_length=1, max_length=100)]
|
||||
|
||||
|
||||
class TaskSafeChart(StrictModel):
|
||||
title: Annotated[str, Field(max_length=200)]
|
||||
chart_type: Literal["line", "bar"]
|
||||
x_data: Annotated[list[Annotated[str, Field(max_length=128)]], Field(min_length=1, max_length=100)]
|
||||
series: Annotated[list[TaskSafeChartSeries], Field(min_length=1, max_length=8)]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_alignment(self):
|
||||
if any(len(series.data) != len(self.x_data) for series in self.series):
|
||||
raise ValueError("chart series must align with x_data")
|
||||
return self
|
||||
|
||||
|
||||
class TaskHighlightFeatures(StrictModel):
|
||||
action: Literal["highlight_features"]
|
||||
layer: TaskMapLayer
|
||||
feature_ids: Annotated[list[TaskFeatureId], Field(min_length=1, max_length=100)]
|
||||
effect: Literal["steady", "pulse"] = "steady"
|
||||
|
||||
|
||||
class TaskClearHighlights(StrictModel):
|
||||
action: Literal["clear_highlights"]
|
||||
|
||||
|
||||
class TaskOpenChart(StrictModel):
|
||||
action: Literal["open_chart"]
|
||||
chart_id: TaskFeatureId
|
||||
chart: TaskSafeChart
|
||||
evidence_refs: Annotated[list[NonEmptyStr], Field(min_length=1, max_length=100)]
|
||||
|
||||
|
||||
class TaskCloseChart(StrictModel):
|
||||
action: Literal["close_chart"]
|
||||
chart_id: TaskFeatureId
|
||||
|
||||
|
||||
class TaskSetOverlay(StrictModel):
|
||||
action: Literal["set_overlay"]
|
||||
overlay_id: TaskFeatureId
|
||||
title: Annotated[str, Field(min_length=1, max_length=200)]
|
||||
mode: Literal["geometry", "heatmap"]
|
||||
evidence_id: NonEmptyStr
|
||||
|
||||
|
||||
class TaskClearOverlay(StrictModel):
|
||||
action: Literal["clear_overlay"]
|
||||
overlay_id: TaskFeatureId
|
||||
|
||||
|
||||
TaskPresentationCommand = Annotated[
|
||||
TaskHighlightFeatures | TaskClearHighlights | TaskOpenChart | TaskCloseChart | TaskSetOverlay | TaskClearOverlay,
|
||||
Field(discriminator="action"),
|
||||
]
|
||||
|
||||
|
||||
class TaskPresentationEventCommand(StrictModel):
|
||||
request_id: UUID
|
||||
run_id: UUID
|
||||
source_session_id: NonEmptyStr
|
||||
command: TaskPresentationCommand
|
||||
|
||||
|
||||
class TaskPresentationEventResult(StrictModel):
|
||||
recorded: Literal[True] = True
|
||||
event_seq: PositiveInt
|
||||
run_id: UUID
|
||||
execution_generation: PositiveInt
|
||||
|
||||
|
||||
class TaskPresentationActiveRun(StrictModel):
|
||||
run_id: UUID
|
||||
generation: PositiveInt
|
||||
status: OperationalTaskRunStatus
|
||||
|
||||
|
||||
class TaskPresentationCursor(StrictModel):
|
||||
task_id: UUID
|
||||
execution_generation: PositiveInt
|
||||
active_run: TaskPresentationActiveRun | None
|
||||
event_cursor: int = Field(ge=0)
|
||||
|
||||
|
||||
class TaskDataQueryResult(StrictModel):
|
||||
|
||||
Reference in New Issue
Block a user