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
163 lines
4.5 KiB
Python
163 lines
4.5 KiB
Python
"""Pure scoring rules for alert-driven investigation tool planning."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from typing import Any, Protocol
|
|
|
|
from core.domain.alerts.alert_source import (
|
|
SECONDARY_TOOL_SOURCES,
|
|
collect_alert_text,
|
|
primary_sources_for_alert,
|
|
relevant_sources_for_alert,
|
|
)
|
|
from core.domain.types.planning import PlannedInvestigationAction
|
|
|
|
FALLBACK_TOOL_NAMES: tuple[str, ...] = ("get_sre_guidance",)
|
|
|
|
|
|
class PlannableTool(Protocol):
|
|
"""Read-only view of the tool fields the alert planner scores against."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def source(self) -> str:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def use_cases(self) -> Sequence[str]:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def examples(self) -> Sequence[str]:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def tags(self) -> Sequence[str]:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def evidence_type(self) -> Any:
|
|
raise NotImplementedError
|
|
|
|
|
|
def score_tools(
|
|
state: dict[str, Any],
|
|
tools: Sequence[PlannableTool],
|
|
) -> list[PlannedInvestigationAction]:
|
|
primary_sources = set(primary_sources_for_alert(state))
|
|
candidate_sources = {str(tool.source) for tool in tools}
|
|
relevant_sources = set(relevant_sources_for_alert(state, candidate_sources))
|
|
alert_text = collect_alert_text(state)
|
|
existing_evidence = state.get("evidence")
|
|
evidence_keys = set(existing_evidence) if isinstance(existing_evidence, dict) else set()
|
|
|
|
scored = [
|
|
score_tool(
|
|
tool,
|
|
alert_text=alert_text,
|
|
primary_sources=primary_sources,
|
|
relevant_sources=relevant_sources,
|
|
evidence_keys=evidence_keys,
|
|
)
|
|
for tool in tools
|
|
]
|
|
if scored and max(action.score for action in scored) <= 0:
|
|
scored = [score_fallback_tool(action) for action in scored]
|
|
|
|
return sorted(
|
|
scored, key=lambda item: (-item.score, item.source in SECONDARY_TOOL_SOURCES, item.name)
|
|
)
|
|
|
|
|
|
def score_tool(
|
|
tool: PlannableTool,
|
|
*,
|
|
alert_text: str,
|
|
primary_sources: set[str],
|
|
relevant_sources: set[str],
|
|
evidence_keys: set[str],
|
|
) -> PlannedInvestigationAction:
|
|
source = str(tool.source)
|
|
score = 0
|
|
reasons: list[str] = []
|
|
|
|
if source in primary_sources:
|
|
score += 100
|
|
reasons.append(f"source '{source}' matches alert source")
|
|
if source in relevant_sources:
|
|
score += 70
|
|
reasons.append(f"source '{source}' matches alert context")
|
|
if source in SECONDARY_TOOL_SOURCES:
|
|
score -= 10
|
|
reasons.append("secondary source, used after integration-specific tools")
|
|
|
|
metadata_text = " ".join(
|
|
[
|
|
tool.description,
|
|
" ".join(tool.use_cases),
|
|
" ".join(tool.examples),
|
|
" ".join(tool.tags),
|
|
str(tool.evidence_type or ""),
|
|
]
|
|
).lower()
|
|
metadata_matches = metadata_matches_for_alert(alert_text, metadata_text)
|
|
if metadata_matches:
|
|
score += min(len(metadata_matches), 5) * 4
|
|
reasons.append(f"metadata matched alert terms: {', '.join(metadata_matches[:5])}")
|
|
|
|
if tool.name in evidence_keys:
|
|
score -= 25
|
|
reasons.append("tool already has evidence in state")
|
|
|
|
if not reasons:
|
|
reasons.append("no source or metadata match")
|
|
|
|
return PlannedInvestigationAction(
|
|
name=tool.name,
|
|
source=source,
|
|
score=score,
|
|
reasons=tuple(reasons),
|
|
)
|
|
|
|
|
|
def metadata_matches_for_alert(alert_text: str, metadata_text: str) -> list[str]:
|
|
if not alert_text or not metadata_text:
|
|
return []
|
|
terms = {
|
|
term.strip(".,:;()[]{}").lower()
|
|
for term in alert_text.split()
|
|
if len(term.strip(".,:;()[]{}")) >= 4
|
|
}
|
|
return sorted(term for term in terms if term in metadata_text)
|
|
|
|
|
|
def score_fallback_tool(
|
|
action: PlannedInvestigationAction,
|
|
) -> PlannedInvestigationAction:
|
|
if action.name not in FALLBACK_TOOL_NAMES:
|
|
return action
|
|
return PlannedInvestigationAction(
|
|
name=action.name,
|
|
source=action.source,
|
|
score=10,
|
|
reasons=(*action.reasons, "included as deterministic fallback"),
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"FALLBACK_TOOL_NAMES",
|
|
"PlannableTool",
|
|
"metadata_matches_for_alert",
|
|
"score_fallback_tool",
|
|
"score_tool",
|
|
"score_tools",
|
|
]
|