Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

286 lines
10 KiB
Python

from __future__ import annotations
import argparse
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import asdict
from pathlib import Path
from typing import Any
from tests.benchmarks.cloudopsbench.case_loader import (
BENCHMARK_DIR,
CLOUDOPSBENCH_HF_DATASET_ID,
CloudOpsCase,
build_alert,
file_sha256,
load_cases,
validate_corpus,
)
from tests.benchmarks.cloudopsbench.replay_backend import CloudOpsBenchReplayBackend
from tests.benchmarks.cloudopsbench.scoring import CloudOpsCaseScore, score_case, summarize_scores
from tools.investigation.capability import run_investigation
DEFAULT_OUTPUT_DIR = Path(".cloudopsbench-results")
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run the Cloud-OpsBench RCA suite in OpenSRE.")
parser.add_argument("--system", default="", help="Filter to boutique or trainticket.")
parser.add_argument("--fault-category", default="", help="Filter to one fault category.")
parser.add_argument("--case", default="", help="Filter to one numeric case directory.")
parser.add_argument("--limit", type=int, default=0, help="Limit cases after sorting/filtering.")
parser.add_argument("--workers", type=int, default=1, help="Number of case workers.")
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON results.")
parser.add_argument(
"--validate-only", action="store_true", help="Validate downloaded corpus only."
)
parser.add_argument(
"--benchmark-dir",
default=str(BENCHMARK_DIR),
help=(
"Downloaded CloudOpsBench benchmark directory. Run "
f"`make download-cloudopsbench-hf` to fetch {CLOUDOPSBENCH_HF_DATASET_ID}."
),
)
parser.add_argument(
"--output-dir",
default=str(DEFAULT_OUTPUT_DIR),
help="Directory for per-case traces and summaries.",
)
parser.add_argument(
"--strict-parity",
action="store_true",
help="Exit non-zero when final answers cannot be parsed as CloudOps top-3 JSON.",
)
return parser.parse_args(argv)
def _json_safe(value: Any) -> Any:
try:
json.dumps(value)
return value
except TypeError:
if isinstance(value, dict):
return {str(k): _json_safe(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [_json_safe(item) for item in value]
return str(value)
def _build_resolved_integrations(
case: CloudOpsCase, backend: CloudOpsBenchReplayBackend
) -> dict[str, Any]:
cluster_name = f"cloudopsbench-{case.system}"
return {
"aws": {
"role_arn": "",
"external_id": "",
"region": "us-east-1",
"cluster_names": [cluster_name],
"_backend": backend,
}
}
def _case_output_path(output_dir: Path, case: CloudOpsCase) -> Path:
return output_dir / case.system / case.fault_category / f"{case.case_name}.json"
def _action_input_from_step(step: str, namespace: str) -> dict[str, Any]:
parts = step.split("::")
action_name = parts[0]
if action_name == "GetResources":
return {"resource_type": parts[1] if len(parts) >= 2 else "pods"}
if action_name == "DescribeResource":
return {
"resource_type": parts[1] if len(parts) >= 2 else "pods",
"name": parts[2] if len(parts) >= 3 else "",
}
if action_name in {"GetErrorLogs", "GetRecentLogs"}:
return {"namespace": namespace, "service_name": parts[1] if len(parts) >= 2 else ""}
if action_name == "GetServiceDependencies":
return {"service_name": parts[1] if len(parts) >= 2 else ""}
if action_name == "GetAppYAML":
return {"app_name": parts[1] if len(parts) >= 2 else ""}
if action_name == "CheckServiceConnectivity":
return {
"service_name": parts[1] if len(parts) >= 2 else "",
"port": int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 80,
}
if action_name == "CheckNodeServiceStatus":
return {
"node_name": parts[1] if len(parts) >= 2 else "master",
"service_name": parts[2] if len(parts) >= 3 else "kube-scheduler",
}
return {}
def _steps_from_backend(backend: CloudOpsBenchReplayBackend) -> list[dict[str, Any]]:
steps: list[dict[str, Any]] = []
for idx, entry in enumerate(backend.action_log, start=1):
steps.append(
{
"step_id": idx,
"action_type": "tool",
"action_name": entry.get("action_name"),
"action_input": entry.get("action_input", {}),
"error": entry.get("error"),
"tool_latency": 0.0,
}
)
return steps
def run_case(case: CloudOpsCase, output_dir: Path) -> tuple[dict[str, Any], CloudOpsCaseScore]:
backend = CloudOpsBenchReplayBackend(case)
alert = build_alert(case)
final_state = run_investigation(
alert,
resolved_integrations=_build_resolved_integrations(case, backend),
)
final_state_dict = _json_safe(dict(final_state))
case_data: dict[str, Any] = {
"case_id": case.case_id,
"system": case.system,
"fault_category": case.fault_category,
"case_name": case.case_name,
"metadata_sha256": file_sha256(case.metadata_path),
"tool_cache_sha256": file_sha256(case.tool_cache_path),
"ground_truth": {
"fault_taxonomy": case.result.fault_taxonomy,
"fault_object": case.result.fault_object,
"root_cause": case.result.root_cause,
},
"final_answer": final_state_dict.get("final_answer") or final_state_dict.get("report"),
"root_cause": final_state_dict.get("root_cause"),
"report": final_state_dict.get("report"),
"expert_steps": {
"path1": list(case.process.get("path1") or []),
"path2": list(case.process.get("path2") or []),
},
"steps": _steps_from_backend(backend),
"final_state": final_state_dict,
}
score = score_case(case, case_data)
payload = {
"case": {
"case_id": case.case_id,
"system": case.system,
"fault_category": case.fault_category,
"case_name": case.case_name,
"metadata_path": str(case.metadata_path),
"tool_cache_path": str(case.tool_cache_path),
},
"run": case_data,
"score": score.to_dict(),
}
output_path = _case_output_path(output_dir, case)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
return payload, score
def _print_validation_report(report: Any) -> None:
if report.ok:
print(f"CloudOpsBench corpus OK: {report.total_cases} cases, {report.file_count} files")
return
print("CloudOpsBench corpus validation failed:")
for error in report.errors:
print(f"- {error}")
def _run_cases(
cases: list[CloudOpsCase], output_dir: Path, workers: int
) -> list[CloudOpsCaseScore]:
scores: list[CloudOpsCaseScore] = []
if workers <= 1:
for case in cases:
_, score = run_case(case, output_dir)
scores.append(score)
return scores
with ThreadPoolExecutor(max_workers=workers) as executor:
future_to_case = {executor.submit(run_case, case, output_dir): case for case in cases}
for future in as_completed(future_to_case):
case = future_to_case[future]
try:
_, score = future.result()
except Exception as exc:
raise RuntimeError(f"{case.case_id}: run failed") from exc
scores.append(score)
return sorted(scores, key=lambda item: item.case_id)
def run_suite(argv: list[str] | None = None) -> list[CloudOpsCaseScore]:
args = parse_args(argv)
output_dir = Path(args.output_dir)
benchmark_dir = Path(args.benchmark_dir)
validation_report = validate_corpus(benchmark_dir)
if args.validate_only:
_print_validation_report(validation_report)
if not validation_report.ok:
raise SystemExit(1)
return []
if not validation_report.ok:
_print_validation_report(validation_report)
raise SystemExit(1)
cases = load_cases(
benchmark_dir,
system=args.system or None,
fault_category=args.fault_category or None,
case_name=args.case or None,
limit=args.limit or None,
)
if not cases:
raise SystemExit("No CloudOpsBench cases matched the requested filters.")
scores = _run_cases(cases, output_dir, max(1, int(args.workers)))
summary = summarize_scores(scores)
summary_payload = {
"benchmark_dir": str(benchmark_dir),
"filters": {
"system": args.system,
"fault_category": args.fault_category,
"case": args.case,
"limit": args.limit,
},
"summary": summary,
"details": [score.to_dict() for score in scores],
}
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "summary.json").write_text(
json.dumps(summary_payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
if args.json:
print(json.dumps(summary_payload, ensure_ascii=False, indent=2))
else:
for score in scores:
metrics = asdict(score.metrics)
print(
f"{score.case_id} A@1={metrics['a1']:.0f} A@3={metrics['a3']:.0f} "
f"TCR={metrics['tcr']:.0f} steps={metrics['steps']:.0f} "
f"error={score.error or '-'}"
)
print(json.dumps(summary["metrics"], ensure_ascii=False, indent=2))
if args.strict_parity and any(score.error for score in scores):
raise SystemExit(1)
return scores
def main(argv: list[str] | None = None) -> int:
run_suite(argv)
return 0
if __name__ == "__main__":
raise SystemExit(main())