feat(investigation): preserve cumulative reference graphs

Accumulate investigation paths and preserve failed attempts across rounds. Keep graph annotations separate from execution state, synchronize the API contract, and consolidate documentation.
This commit is contained in:
2026-11-11 19:10:41 +08:00
parent e47c61c121
commit b853b61a0b
38 changed files with 2445 additions and 147 deletions
+32 -2
View File
@@ -7,7 +7,9 @@ from uuid import UUID
from psycopg import AsyncConnection
from psycopg import sql
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from fastapi import HTTPException
from app.domain.schemas.operational_task import LEGACY_INVESTIGATION_NODE_ANNOTATIONS
from app.infra.db.postgresql.scada import _SCADA_VIEW_SELECT, _device
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
@@ -329,6 +331,29 @@ async def task_context_summary(conn: AsyncConnection, *, project_id: UUID, task_
return result
def _reference_diagram_projection(stored_sql: str, stored_params: tuple, section: str) -> tuple[str, tuple]:
"""Normalize only saved diagram annotations before any data_path or page projection."""
if section not in {"progress", "report"}:
raise ValueError("reference diagrams only belong to progress or report sections")
path = "{data," + section + ",diagram,node_refs}"
statement = f"""
WITH reference_documents AS (
{stored_sql}
), reference_aliases AS (
SELECT %s::jsonb AS annotations
)
SELECT CASE WHEN jsonb_typeof(snapshot#>'{path}')='array' THEN
jsonb_set(snapshot,'{path}',COALESCE((
SELECT jsonb_agg(CASE WHEN annotations ? (node->>'status')
THEN jsonb_set(node,'{{status}}',annotations->(node->>'status'))
ELSE node END ORDER BY position)
FROM jsonb_array_elements(snapshot#>'{path}') WITH ORDINALITY AS nodes(node,position)
),'[]'::jsonb)) ELSE snapshot END AS snapshot
FROM reference_documents CROSS JOIN reference_aliases
"""
return statement, (*stored_params, Jsonb(LEGACY_INVESTIGATION_NODE_ANNOTATIONS))
async def task_context_page(
conn: AsyncConnection, *, project_id: UUID, task_id: UUID, section: str,
report_revision: int | None, data_path: list[str], limit: int, offset: int,
@@ -386,11 +411,16 @@ async def task_context_page(
stored = """SELECT jsonb_build_object('section','progress','data',p.document) AS snapshot
FROM workbench.operational_task_progress p
JOIN workbench.operational_task_runs r ON r.project_id=p.project_id AND r.task_id=p.task_id AND r.run_id=p.run_id
JOIN workbench.operational_tasks t ON t.project_id=p.project_id AND t.task_id=p.task_id AND t.execution_generation=r.generation
WHERE p.project_id=%s AND p.task_id=%s"""
JOIN workbench.operational_tasks t ON t.project_id=p.project_id AND t.task_id=p.task_id
WHERE p.project_id=%s AND p.task_id=%s AND r.generation<=t.execution_generation
AND (t.execution_generation=r.generation
OR jsonb_typeof(p.document#>'{progress,diagram,edges}')='array')
ORDER BY (t.execution_generation=r.generation) DESC,r.generation DESC,p.revision DESC LIMIT 1"""
params = (project_id, task_id)
else:
raise HTTPException(422, "Unknown task context section")
if section in {"progress", "report"}:
stored, params = _reference_diagram_projection(stored, params, section)
return await _snapshot_page(conn, stored_sql=stored, stored_params=params,
data_path=data_path, limit=limit, offset=projection_offset, mcp_sources=[], max_bytes=8000)