4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
394 lines
14 KiB
Python
394 lines
14 KiB
Python
# ======== from tools/prefect_flow_runs_tool/ ========
|
|
|
|
"""Prefect failed flow runs investigation tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from core.tool_framework.base import BaseTool
|
|
from integrations.prefect.client import make_prefect_client
|
|
|
|
_ERROR_KEYWORDS = ("error", "failed", "exception", "fatal", "crash", "traceback", "exitcode")
|
|
_FAILED_STATES = {"FAILED", "CRASHED", "CANCELLED", "CANCELLING"}
|
|
|
|
|
|
class PrefectFlowRunsTool(BaseTool):
|
|
"""Fetch and triage recent Prefect flow runs, surfacing failures for RCA."""
|
|
|
|
name = "prefect_flow_runs"
|
|
source = "prefect"
|
|
description = (
|
|
"Fetch recent Prefect flow runs filtered by state, and retrieve logs for failed runs "
|
|
"to surface orchestration failures and root-cause evidence."
|
|
)
|
|
use_cases = [
|
|
"Investigating why a Prefect flow run failed or crashed",
|
|
"Listing all recent FAILED or CRASHED flow runs for triage",
|
|
"Fetching logs from a specific failed flow run",
|
|
"Correlating Prefect flow failures with infrastructure alerts",
|
|
"Identifying recurring flow failures across deployments",
|
|
]
|
|
requires = ["api_url"]
|
|
injected_params = ["api_key", "api_url", "workspace_id"]
|
|
input_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"api_url": {
|
|
"type": "string",
|
|
"description": (
|
|
"Prefect API base URL. Use https://api.prefect.cloud/api for Prefect Cloud "
|
|
"or your self-hosted server URL (e.g. http://localhost:4200/api)."
|
|
),
|
|
},
|
|
"api_key": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "Prefect Cloud API key. Leave empty for self-hosted servers with no auth.",
|
|
},
|
|
"account_id": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "Prefect Cloud account ID (required for Prefect Cloud).",
|
|
},
|
|
"workspace_id": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "Prefect Cloud workspace ID (required for Prefect Cloud).",
|
|
},
|
|
"states": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": ["FAILED", "CRASHED"],
|
|
"description": "Flow run states to filter on. Defaults to FAILED and CRASHED.",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"default": 20,
|
|
"description": "Maximum number of flow runs to return.",
|
|
},
|
|
"fetch_logs_for_run_id": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": (
|
|
"Optional flow run ID to fetch detailed logs for. "
|
|
"Use after identifying a specific failed run."
|
|
),
|
|
},
|
|
"log_limit": {
|
|
"type": "integer",
|
|
"default": 100,
|
|
"description": "Maximum number of log lines to fetch per flow run.",
|
|
},
|
|
},
|
|
"required": ["api_url"],
|
|
}
|
|
outputs = {
|
|
"flow_runs": "List of matching flow runs with state and timing metadata",
|
|
"failed_runs": "Subset of runs in FAILED or CRASHED state",
|
|
"logs": "Log lines for the requested flow run (if fetch_logs_for_run_id is set)",
|
|
"error_log_lines": "Log lines containing error keywords",
|
|
}
|
|
|
|
def is_available(self, sources: dict[str, Any]) -> bool:
|
|
return bool(sources.get("prefect", {}).get("connection_verified"))
|
|
|
|
def extract_params(self, sources: dict[str, Any]) -> dict[str, Any]:
|
|
prefect = sources.get("prefect", {})
|
|
return {
|
|
"api_url": prefect.get("api_url", ""),
|
|
"api_key": prefect.get("api_key", ""),
|
|
"account_id": prefect.get("account_id", ""),
|
|
"workspace_id": prefect.get("workspace_id", ""),
|
|
"states": ["FAILED", "CRASHED"],
|
|
"limit": 20,
|
|
"fetch_logs_for_run_id": "",
|
|
"log_limit": 100,
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
api_url: str,
|
|
api_key: str = "",
|
|
account_id: str = "",
|
|
workspace_id: str = "",
|
|
states: list[str] | None = None,
|
|
limit: int = 20,
|
|
fetch_logs_for_run_id: str = "",
|
|
log_limit: int = 100,
|
|
**_kwargs: Any,
|
|
) -> dict[str, Any]:
|
|
if not (api_url or "").strip():
|
|
return {
|
|
"source": "prefect",
|
|
"available": False,
|
|
"error": "api_url is required to connect to Prefect.",
|
|
"flow_runs": [],
|
|
"failed_runs": [],
|
|
"logs": [],
|
|
"error_log_lines": [],
|
|
}
|
|
|
|
client = make_prefect_client(
|
|
api_url=api_url,
|
|
api_key=api_key,
|
|
account_id=account_id,
|
|
workspace_id=workspace_id,
|
|
)
|
|
if client is None:
|
|
return {
|
|
"source": "prefect",
|
|
"available": False,
|
|
"error": "Prefect integration could not be initialized. Check your api_url.",
|
|
"flow_runs": [],
|
|
"failed_runs": [],
|
|
"logs": [],
|
|
"error_log_lines": [],
|
|
}
|
|
|
|
effective_states = states if states is not None else ["FAILED", "CRASHED"]
|
|
|
|
with client:
|
|
runs_result = client.get_flow_runs(limit=limit, states=effective_states)
|
|
if not runs_result.get("success"):
|
|
return {
|
|
"source": "prefect",
|
|
"available": False,
|
|
"error": runs_result.get("error", "Unknown error fetching flow runs."),
|
|
"flow_runs": [],
|
|
"failed_runs": [],
|
|
"logs": [],
|
|
"error_log_lines": [],
|
|
}
|
|
|
|
flow_runs: list[dict[str, Any]] = runs_result.get("flow_runs", [])
|
|
failed_runs = [
|
|
r for r in flow_runs if r.get("state_type", "").upper() in _FAILED_STATES
|
|
]
|
|
|
|
logs: list[dict[str, Any]] = []
|
|
error_log_lines: list[dict[str, Any]] = []
|
|
|
|
logs_error: str | None = None
|
|
if fetch_logs_for_run_id:
|
|
logs_result = client.get_flow_run_logs(
|
|
flow_run_id=fetch_logs_for_run_id, limit=log_limit
|
|
)
|
|
if logs_result.get("success"):
|
|
logs = logs_result.get("logs", [])
|
|
error_log_lines = [
|
|
line
|
|
for line in logs
|
|
if any(kw in line.get("message", "").lower() for kw in _ERROR_KEYWORDS)
|
|
]
|
|
else:
|
|
logs_error = logs_result.get("error", "Unknown error fetching logs.")
|
|
|
|
result: dict[str, Any] = {
|
|
"source": "prefect",
|
|
"available": True,
|
|
"flow_runs": flow_runs,
|
|
"total": len(flow_runs),
|
|
"failed_runs": failed_runs,
|
|
"total_failed": len(failed_runs),
|
|
"logs": logs,
|
|
"error_log_lines": error_log_lines,
|
|
"fetched_logs_for_run_id": fetch_logs_for_run_id or None,
|
|
}
|
|
if logs_error is not None:
|
|
result["logs_error"] = logs_error
|
|
return result
|
|
|
|
|
|
prefect_flow_runs = PrefectFlowRunsTool()
|
|
|
|
|
|
# ======== from tools/prefect_worker_health_tool/ ========
|
|
|
|
"""Prefect worker and work pool health investigation tool."""
|
|
|
|
|
|
from core.tool_framework.base import BaseTool
|
|
|
|
_UNHEALTHY_WORKER_STATUSES = {"OFFLINE", "UNHEALTHY"}
|
|
_UNHEALTHY_POOL_STATUSES = {"NOT_READY", "PAUSED"}
|
|
|
|
|
|
class PrefectWorkerHealthTool(BaseTool):
|
|
"""Inspect Prefect work pool and worker health to identify orchestration bottlenecks."""
|
|
|
|
name = "prefect_worker_health"
|
|
source = "prefect"
|
|
description = (
|
|
"Inspect Prefect work pools and their registered workers to identify offline, "
|
|
"unhealthy, or paused workers that may be blocking flow run execution."
|
|
)
|
|
use_cases = [
|
|
"Diagnosing why Prefect flows are stuck in PENDING state",
|
|
"Identifying offline or unresponsive Prefect workers",
|
|
"Checking which work pools are paused or have no active workers",
|
|
"Investigating worker heartbeat failures",
|
|
"Auditing work pool concurrency limits during incident investigation",
|
|
]
|
|
requires = ["api_url"]
|
|
injected_params = ["api_key", "api_url", "workspace_id"]
|
|
input_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"api_url": {
|
|
"type": "string",
|
|
"description": (
|
|
"Prefect API base URL. Use https://api.prefect.cloud/api for Prefect Cloud "
|
|
"or your self-hosted server URL (e.g. http://localhost:4200/api)."
|
|
),
|
|
},
|
|
"api_key": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "Prefect Cloud API key. Leave empty for self-hosted servers with no auth.",
|
|
},
|
|
"account_id": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "Prefect Cloud account ID (required for Prefect Cloud).",
|
|
},
|
|
"workspace_id": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "Prefect Cloud workspace ID (required for Prefect Cloud).",
|
|
},
|
|
"work_pool_name": {
|
|
"type": "string",
|
|
"default": "",
|
|
"description": (
|
|
"Name of a specific work pool to inspect workers for. "
|
|
"If omitted, lists all work pools without drilling into workers."
|
|
),
|
|
},
|
|
"pool_limit": {
|
|
"type": "integer",
|
|
"default": 20,
|
|
"description": "Maximum number of work pools to list.",
|
|
},
|
|
"worker_limit": {
|
|
"type": "integer",
|
|
"default": 20,
|
|
"description": "Maximum number of workers to list per work pool.",
|
|
},
|
|
},
|
|
"required": ["api_url"],
|
|
}
|
|
outputs = {
|
|
"work_pools": "All listed work pools with status and pause state",
|
|
"unhealthy_pools": "Work pools that are paused or in NOT_READY state",
|
|
"workers": "Workers registered in the requested work pool",
|
|
"unhealthy_workers": "Workers that are OFFLINE or UNHEALTHY",
|
|
}
|
|
|
|
def is_available(self, sources: dict[str, Any]) -> bool:
|
|
return bool(sources.get("prefect", {}).get("connection_verified"))
|
|
|
|
def extract_params(self, sources: dict[str, Any]) -> dict[str, Any]:
|
|
prefect = sources.get("prefect", {})
|
|
return {
|
|
"api_url": prefect.get("api_url", ""),
|
|
"api_key": prefect.get("api_key", ""),
|
|
"account_id": prefect.get("account_id", ""),
|
|
"workspace_id": prefect.get("workspace_id", ""),
|
|
"work_pool_name": prefect.get("work_pool_name", ""),
|
|
"pool_limit": 20,
|
|
"worker_limit": 20,
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
api_url: str,
|
|
api_key: str = "",
|
|
account_id: str = "",
|
|
workspace_id: str = "",
|
|
work_pool_name: str = "",
|
|
pool_limit: int = 20,
|
|
worker_limit: int = 20,
|
|
**_kwargs: Any,
|
|
) -> dict[str, Any]:
|
|
if not (api_url or "").strip():
|
|
return {
|
|
"source": "prefect",
|
|
"available": False,
|
|
"error": "api_url is required to connect to Prefect.",
|
|
"work_pools": [],
|
|
"unhealthy_pools": [],
|
|
"workers": [],
|
|
"unhealthy_workers": [],
|
|
}
|
|
|
|
client = make_prefect_client(
|
|
api_url=api_url,
|
|
api_key=api_key,
|
|
account_id=account_id,
|
|
workspace_id=workspace_id,
|
|
)
|
|
if client is None:
|
|
return {
|
|
"source": "prefect",
|
|
"available": False,
|
|
"error": "Prefect integration could not be initialized. Check your api_url.",
|
|
"work_pools": [],
|
|
"unhealthy_pools": [],
|
|
"workers": [],
|
|
"unhealthy_workers": [],
|
|
}
|
|
|
|
with client:
|
|
pools_result = client.get_work_pools(limit=pool_limit)
|
|
if not pools_result.get("success"):
|
|
return {
|
|
"source": "prefect",
|
|
"available": False,
|
|
"error": pools_result.get("error", "Unknown error fetching work pools."),
|
|
"work_pools": [],
|
|
"unhealthy_pools": [],
|
|
"workers": [],
|
|
"unhealthy_workers": [],
|
|
}
|
|
|
|
work_pools: list[dict[str, Any]] = pools_result.get("work_pools", [])
|
|
unhealthy_pools = [
|
|
p
|
|
for p in work_pools
|
|
if p.get("status", "").upper() in _UNHEALTHY_POOL_STATUSES
|
|
or p.get("is_paused", False)
|
|
]
|
|
|
|
workers: list[dict[str, Any]] = []
|
|
unhealthy_workers: list[dict[str, Any]] = []
|
|
|
|
if work_pool_name:
|
|
workers_result = client.get_workers(
|
|
work_pool_name=work_pool_name, limit=worker_limit
|
|
)
|
|
if workers_result.get("success"):
|
|
workers = workers_result.get("workers", [])
|
|
unhealthy_workers = [
|
|
w
|
|
for w in workers
|
|
if w.get("status", "").upper() in _UNHEALTHY_WORKER_STATUSES
|
|
]
|
|
|
|
return {
|
|
"source": "prefect",
|
|
"available": True,
|
|
"work_pools": work_pools,
|
|
"total_pools": len(work_pools),
|
|
"unhealthy_pools": unhealthy_pools,
|
|
"total_unhealthy_pools": len(unhealthy_pools),
|
|
"work_pool_name": work_pool_name or None,
|
|
"workers": workers,
|
|
"total_workers": len(workers),
|
|
"unhealthy_workers": unhealthy_workers,
|
|
"total_unhealthy_workers": len(unhealthy_workers),
|
|
}
|
|
|
|
|
|
prefect_worker_health = PrefectWorkerHealthTool()
|