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:
@@ -628,6 +628,37 @@ class OperationalTaskRepository:
|
||||
row = await cursor.fetchone()
|
||||
return row["document"] if row else None
|
||||
|
||||
async def get_latest_graph_progress(self, task_id: str) -> dict | None:
|
||||
availability = await self.connection.execute(
|
||||
"SELECT to_regclass('workbench.operational_task_progress') AS relation"
|
||||
)
|
||||
if not (await availability.fetchone())["relation"]:
|
||||
return None
|
||||
cursor = await self.connection.execute(
|
||||
"""SELECT document FROM workbench.operational_task_progress
|
||||
WHERE project_id=%s AND task_id=%s
|
||||
AND jsonb_typeof(document#>'{progress,diagram,edges}')='array'
|
||||
ORDER BY (document->>'execution_generation')::bigint DESC, revision DESC LIMIT 1""",
|
||||
(self.project_id, task_id),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return row["document"] if row else None
|
||||
|
||||
async def get_latest_diagram(self, task_id: str) -> dict | None:
|
||||
cursor = await self.connection.execute(
|
||||
"""SELECT diagram FROM (
|
||||
SELECT document#>'{progress,diagram}' AS diagram, published_at AS occurred_at
|
||||
FROM workbench.operational_task_progress WHERE project_id=%s AND task_id=%s
|
||||
UNION ALL
|
||||
SELECT document#>'{report,diagram}', created_at
|
||||
FROM workbench.operational_task_reports WHERE project_id=%s AND task_id=%s
|
||||
) AS diagrams WHERE jsonb_typeof(diagram)='object'
|
||||
ORDER BY occurred_at DESC LIMIT 1""",
|
||||
(self.project_id, task_id, self.project_id, task_id),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return row["diagram"] if row else None
|
||||
|
||||
async def save_progress(self, task_id: str, snapshot: dict) -> None:
|
||||
cursor = await self.connection.execute(
|
||||
"""INSERT INTO workbench.operational_task_progress
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user