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:
2026-10-19 18:46:58 +08:00
parent 2b6fc5aa52
commit be6317eb48
117 changed files with 12703 additions and 638 deletions
+119 -6
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Any
from psycopg import AsyncConnection
@@ -11,6 +12,7 @@ from uuid import uuid4
from app.domain.domain_errors import DomainError
from app.infra.db.postgresql.workbench_events import lock_event_transaction
from app.infra.db.postgresql.workbench_events import WorkbenchEventRepository
class OperationalTaskRepository:
@@ -37,6 +39,13 @@ class OperationalTaskRepository:
(f"tjwater-operational:{self.project_id}:{key}",),
)
async def try_lock(self, key: str) -> bool:
cursor = await self.connection.execute(
"SELECT pg_try_advisory_xact_lock(hashtextextended(%s, 0)) AS acquired",
(f"tjwater-operational:{self.project_id}:{key}",),
)
return bool((await cursor.fetchone())["acquired"])
async def get_request(self, scope: str, request_id: str) -> dict | None:
cursor = await self.connection.execute(
"""SELECT request_digest,response
@@ -103,6 +112,50 @@ class OperationalTaskRepository:
row = await cursor.fetchone()
return dict(row) if row else None
async def list_task_runs(self, task_id: str, limit: int, offset: int) -> list[dict]:
cursor = await self.connection.execute(
"""SELECT * FROM workbench.operational_task_runs
WHERE project_id=%s AND task_id=%s
ORDER BY generation DESC LIMIT %s OFFSET %s""",
(self.project_id, task_id, limit, offset),
)
return [{key: value for key, value in row.items() if key != "project_id"}
for row in await cursor.fetchall()]
async def get_latest_run(self, task_id: str) -> dict | None:
rows = await self.list_task_runs(task_id, 1, 0)
return rows[0] if rows else None
async def require_execution_storage(self) -> None:
"""Reject claims before changing state if the inbox cannot be used."""
cursor = await self.connection.execute(
"SELECT to_regclass('workbench.agent_messages') IS NOT NULL AS available"
)
if not (await cursor.fetchone())["available"]:
raise DomainError(
"DEPENDENCY_UNAVAILABLE",
"Agent message storage is not provisioned; apply "
"resources/sql/024_agent_messages.sql to this project database before starting runs",
status=503,
)
async def pending_run_messages(self, run_id: str) -> list[str]:
cursor = await self.connection.execute(
"""SELECT message_id FROM workbench.agent_messages
WHERE project_id=%s AND run_id=%s AND direction='to_task'
AND status IN ('pending','enqueued') ORDER BY created_at LIMIT 100""",
(self.project_id, run_id),
)
return [str(row["message_id"]) for row in await cursor.fetchall()]
async def fail_run_messages(self, run_id: str, failure_detail: str, now: datetime) -> None:
await self.connection.execute(
"""UPDATE workbench.agent_messages SET status='failed',failure_detail=%s,updated_at=%s
WHERE project_id=%s AND run_id=%s AND direction='to_task'
AND status IN ('pending','enqueued')""",
(failure_detail, now, self.project_id, run_id),
)
async def insert_run(self, run: dict) -> None:
await self.connection.execute(
"""INSERT INTO workbench.operational_task_runs
@@ -142,7 +195,8 @@ class OperationalTaskRepository:
raise DomainError("VERSION_CONFLICT", "Run revision changed", status=409)
async def list_runs(
self, *, status: str | None, limit: int, cursor_created_at=None, cursor_run_id=None
self, *, status: str | None, limit: int, cursor_created_at=None, cursor_run_id=None,
task_type: str | None = None, task_created_from: datetime | None = None,
) -> list[dict]:
result = await self.connection.execute(
"""SELECT run_id,task_id,generation,execution_request_id,status,revision,
@@ -154,15 +208,25 @@ class OperationalTaskRepository:
AND (%s::text IS NULL OR status=%s::text)
AND (%s::timestamptz IS NULL OR (created_at,run_id)>(%s::timestamptz,%s::uuid))
AND (status<>'queued' OR next_dispatch_at<=now())
AND EXISTS (
SELECT 1 FROM workbench.operational_tasks t
WHERE t.project_id=operational_task_runs.project_id
AND t.task_id=operational_task_runs.task_id
AND (%s::text IS NULL OR t.task_type=%s::text)
AND (%s::timestamptz IS NULL OR t.created_at>=%s::timestamptz)
)
ORDER BY created_at,run_id LIMIT %s""",
(
self.project_id, status, status, cursor_created_at,
cursor_created_at, cursor_run_id, limit,
cursor_created_at, cursor_run_id,
task_type, task_type, task_created_from, task_created_from, limit,
),
)
return [dict(row) for row in await result.fetchall()]
async def claim_expired_runs(self, limit: int) -> list[dict]:
async def claim_expired_runs(
self, limit: int, *, task_type: str | None = None, task_created_from: datetime | None = None,
) -> list[dict]:
cursor = await self.connection.execute(
"""SELECT run_id,task_id,generation,execution_request_id,status,revision,
agent_session_id,lease_expires_at,heartbeat_at,started_at,finished_at,
@@ -171,8 +235,15 @@ class OperationalTaskRepository:
FROM workbench.operational_task_runs
WHERE project_id=%s AND status IN ('claimed','running')
AND lease_expires_at IS NOT NULL AND lease_expires_at<=now()
AND EXISTS (
SELECT 1 FROM workbench.operational_tasks t
WHERE t.project_id=operational_task_runs.project_id
AND t.task_id=operational_task_runs.task_id
AND (%s::text IS NULL OR t.task_type=%s::text)
AND (%s::timestamptz IS NULL OR t.created_at>=%s::timestamptz)
)
ORDER BY lease_expires_at,run_id FOR UPDATE SKIP LOCKED LIMIT %s""",
(self.project_id, limit),
(self.project_id, task_type, task_type, task_created_from, task_created_from, limit),
)
return [dict(row) for row in await cursor.fetchall()]
@@ -212,6 +283,9 @@ class OperationalTaskRepository:
)
return event_seq
async def current_event_cursor(self) -> int:
return await WorkbenchEventRepository(self.connection, self.project_id).current_cursor()
async def get_task_by_create_request(self, request_id: str) -> dict | None:
cursor = await self.connection.execute(
"""SELECT document FROM workbench.operational_tasks
@@ -246,7 +320,9 @@ class OperationalTaskRepository:
cursor = await self.connection.execute(
"""SELECT task.document,
CASE WHEN active_run.run_id IS NULL THEN NULL
ELSE to_jsonb(active_run) - 'project_id' END AS active_run
ELSE to_jsonb(active_run) - 'project_id' END AS active_run,
CASE WHEN latest_run.run_id IS NULL THEN NULL
ELSE to_jsonb(latest_run) - 'project_id' END AS latest_run
FROM workbench.operational_tasks AS task
LEFT JOIN LATERAL (
SELECT run.* FROM workbench.operational_task_runs AS run
@@ -254,6 +330,11 @@ class OperationalTaskRepository:
AND run.status IN ('queued','claimed','running')
ORDER BY run.generation DESC LIMIT 1
) AS active_run ON true
LEFT JOIN LATERAL (
SELECT run.* FROM workbench.operational_task_runs AS run
WHERE run.project_id=task.project_id AND run.task_id=task.task_id
ORDER BY run.generation DESC LIMIT 1
) AS latest_run ON true
WHERE task.project_id=%s
AND (task.document#>>'{source,deleted_at}') IS NULL
AND (%s::text IS NULL OR task.task_type=%s::text)
@@ -285,7 +366,7 @@ class OperationalTaskRepository:
),
)
return [
{**row["document"], "active_run": row["active_run"]}
{**row["document"], "active_run": row["active_run"], "latest_run": row["latest_run"]}
for row in await cursor.fetchall()
]
@@ -444,6 +525,38 @@ class OperationalTaskRepository:
async def insert_report(self, task_id: str, snapshot: dict) -> None:
await self._insert_artifact("operational_task_reports", task_id, snapshot)
async def get_latest_progress(self, task_id: str, generation: int) -> dict | None:
# Read compatibility before the coordinated progress migration is applied.
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 progress.document FROM workbench.operational_task_progress AS progress
JOIN workbench.operational_task_runs AS run
ON run.project_id=progress.project_id AND run.run_id=progress.run_id
AND run.task_id=progress.task_id
WHERE progress.project_id=%s AND progress.task_id=%s AND run.generation=%s""",
(self.project_id, task_id, generation),
)
row = await cursor.fetchone()
return row["document"] 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
(project_id,task_id,run_id,revision,document,published_at)
VALUES (%s,%s,%s,%s,%s,%s)
ON CONFLICT(project_id,task_id,run_id) DO UPDATE SET
revision=excluded.revision,document=excluded.document,published_at=excluded.published_at
WHERE workbench.operational_task_progress.revision=excluded.revision-1""",
(self.project_id, task_id, snapshot["run_id"], snapshot["revision"],
Jsonb(snapshot), snapshot["published_at"]),
)
if cursor.rowcount != 1:
raise DomainError("VERSION_CONFLICT", "Investigation progress revision changed", status=409)
async def _insert_artifact(self, table: str, task_id: str, snapshot: dict) -> None:
if table not in {"operational_task_plans", "operational_task_reports"}:
raise ValueError("unsupported artifact table")