Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3fb413c
Enhance unit test script and CI pipeline for AI failure analysis
chensuyue May 29, 2026
ca39038
for test only, need to revert before merge
chensuyue May 29, 2026
ec61ccb
temp version
chensuyue Jun 5, 2026
32fecde
add issue group
chensuyue Jun 6, 2026
93d4511
fix know issue label
chensuyue Jul 9, 2026
3ab11ff
Merge branch 'main' into suyue/ai4ci
chensuyue Jul 9, 2026
8e1a086
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
c06e89b
update dict
chensuyue Jul 9, 2026
99b0b84
update scripts
chensuyue Jul 9, 2026
3b9d1e8
for debug
chensuyue Jul 9, 2026
2e28fd2
refactor: remove ci_part and failure_log_context parameters; update f…
chensuyue Jul 9, 2026
c40ef7f
test classify first
chensuyue Jul 9, 2026
c373fc3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
c51cda6
bug fix
chensuyue Jul 9, 2026
42f728e
Merge branch 'suyue/ai4ci' of https://github.com/intel/auto-round int…
chensuyue Jul 9, 2026
2211092
refactor: remove summary_log parameter from FailureContextWriter and …
chensuyue Jul 9, 2026
b53bc68
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
a064c01
fix: update display name for Classify job and set pool for AI Analysi…
chensuyue Jul 9, 2026
db7e78d
remove tail in context json file
chensuyue Jul 9, 2026
10375f8
fix: adjust line selection logic in FailureContextWriter to prevent i…
chensuyue Jul 10, 2026
dbededd
Merge branch 'main' into suyue/ai4ci
chensuyue Jul 10, 2026
bad76b0
Merge branch 'suyue/ai4ci' of https://github.com/intel/auto-round int…
chensuyue Jul 10, 2026
0e97dbe
remove version limit to trigger more issues
chensuyue Jul 10, 2026
2256571
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
557 changes: 557 additions & 0 deletions .azure-pipelines/scripts/ai_failure_analysis/analyze_and_suggest.py

Large diffs are not rendered by default.

241 changes: 241 additions & 0 deletions .azure-pipelines/scripts/ai_failure_analysis/classify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
"""Classify a CI failure into one of six categories and route handling.

Pipeline position: runs after ``merge_failure_context.py`` and before any
category-specific handling. It combines deterministic forensic evidence
(``evidence_collectors``) and known-issue matches (``known_issue_matcher``), then
emits Azure DevOps pipeline variables so downstream steps can route handling.

Classification categories (priority order mirrors the triage flow):
1. Known Issue 2. Environment 3. Dependency
4. Flaky Test 5. Code Regression 6. Other
"""

import argparse
import json
import os
from pathlib import Path

import evidence_collectors
import known_issue_matcher

# Canonical category labels.
KNOWN_ISSUE = "Known Issue"
ENVIRONMENT = "Environment"
DEPENDENCY = "Dependency"
FLAKY = "Flaky Test"
CODE_REGRESSION = "Code Regression"
OTHER = "Other"

VALID_CATEGORIES = {KNOWN_ISSUE, ENVIRONMENT, DEPENDENCY, FLAKY, CODE_REGRESSION, OTHER}

# Below this confidence the result is forced to ``Other`` and the CI admin is
# pinged, to avoid mis-routing.
CONFIDENCE_THRESHOLD = 0.5

# A known-issue match at or above this confidence short-circuits to Known Issue.
KNOWN_ISSUE_AUTO_THRESHOLD = 0.6

# Handling action keys consumed by post_pr_comment.py / template gating.
HANDLING_BY_CATEGORY = {
KNOWN_ISSUE: "comment_known_issue",
ENVIRONMENT: "comment_environment",
DEPENDENCY: "comment_dependency",
FLAKY: "comment_flaky",
CODE_REGRESSION: "analyze_regression",
OTHER: "comment_other_notify_admin",
}


def load_json(path: Path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)


def write_json(path: Path, payload: dict):
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)


def emit_pipeline_variable(name: str, value: str):
"""Emit an Azure DevOps logging command to set a pipeline variable.

Variable is consumable by later steps in the same job via ``$(NAME)`` /
``variables['NAME']``.
"""
safe = str(value).replace("\r", " ").replace("\n", " ")
print(f"##vso[task.setvariable variable={name}]{safe}")


def _group_pr_relevance(evidence: dict, group_id: str) -> dict:
for item in evidence.get("pr_relevance", {}).get("per_group", []):
if item.get("group_id") == group_id:
return item
return {}


def heuristic_classification(group: dict, evidence: dict, known_matches: list[dict]) -> dict:
"""Deterministic group-level classification."""
if known_matches and known_matches[0].get("confidence", 0.0) >= KNOWN_ISSUE_AUTO_THRESHOLD:
return {
"classification": KNOWN_ISSUE,
"confidence": known_matches[0]["confidence"],
"evidence": [f"matches known issue #{known_matches[0].get('number')}"],
"reasoning": "Known-issue matcher returned a high-confidence ticket.",
}

if group.get("signature_type") == "env_signal":
signature = group.get("signature", "")
signal = signature.split(":", 1)[1] if ":" in signature else signature
return {
"classification": ENVIRONMENT,
"confidence": 0.7,
"evidence": [f"environment signal: {signal}"],
"reasoning": "Group signature indicates infrastructure/environment symptoms.",
}

pr_group = _group_pr_relevance(evidence, group.get("group_id", ""))
pr_global = evidence.get("pr_relevance", {})
if pr_group.get("relevance_score", 0.0) >= 0.5 and pr_global.get("touches_source"):
return {
"classification": CODE_REGRESSION,
"confidence": round(min(0.9, 0.5 + pr_group["relevance_score"] / 2), 3),
"evidence": [
f"group_relevance_score={pr_group.get('relevance_score')}",
f"directly_changed_tests={pr_group.get('directly_changed_tests', [])[:5]}",
],
"reasoning": "This group correlates with PR-changed source or tests.",
}

return {
"classification": OTHER,
"confidence": 0.3,
"evidence": ["no strong deterministic signal"],
"reasoning": "Insufficient evidence for confident classification.",
}


def finalize_classification(raw_result: dict, known_matches: list[dict], admin_handle: str) -> dict:
"""Apply guard rails: known-issue short-circuit and confidence threshold."""
classification = str(raw_result.get("classification", "")).strip()
if classification not in VALID_CATEGORIES:
classification = OTHER

try:
confidence = float(raw_result.get("confidence", 0.0))
except (TypeError, ValueError):
confidence = 0.0
confidence = max(0.0, min(1.0, confidence))

evidence = raw_result.get("evidence", []) or []
reasoning = raw_result.get("reasoning", "")

notify_admin = False
matches = known_matches

# Guard 1: strong known-issue match always wins.
if matches and matches[0].get("confidence", 0.0) >= KNOWN_ISSUE_AUTO_THRESHOLD:
classification = KNOWN_ISSUE
confidence = max(confidence, matches[0]["confidence"])

# Guard 2: low-confidence results fall back to Other and ping the admin.
if classification != KNOWN_ISSUE and confidence < CONFIDENCE_THRESHOLD:
classification = OTHER
notify_admin = True
reasoning = (
f"Confidence {confidence} below threshold {CONFIDENCE_THRESHOLD}; "
f"routed to Other for manual review. {reasoning}"
).strip()

if classification == OTHER:
notify_admin = True

handling_action = HANDLING_BY_CATEGORY.get(classification, HANDLING_BY_CATEGORY[OTHER])

return {
"classification": classification,
"confidence": round(confidence, 3),
"evidence": evidence[:10],
"reasoning": reasoning,
"handling_action": handling_action,
"notify_admin": notify_admin,
"ci_admin_handle": admin_handle,
"known_issue_matches": matches[:5],
}


def main():
parser = argparse.ArgumentParser(description="Classify CI failure and route handling (deterministic)")
parser.add_argument("--failure-context", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--project-root", type=Path, default=Path.cwd())
parser.add_argument("--base-ref", default="main")
parser.add_argument("--known-issue-label", default=known_issue_matcher.DEFAULT_LABEL)
parser.add_argument("--admin-handle", default=os.environ.get("CI_ADMIN_HANDLE", "chensuyue"))
args = parser.parse_args()

project_root = args.project_root.resolve()
payload = load_json(args.failure_context)
groups = payload.get("groups", [])

evidence = evidence_collectors.collect_all_evidence(groups, project_root, args.base_ref)

repo_path = known_issue_matcher._repo_path_from_env()
token = os.environ.get("GITHUB_TOKEN", "")
known_issues = known_issue_matcher.match_known_issues(groups, repo_path, token, label=args.known_issue_label)

known_map = {
item.get("group_id", ""): item.get("matches", []) for item in known_issues.get("per_group_matches", [])
}

per_group_results = []
category_counts = {key: 0 for key in VALID_CATEGORIES}

for group in groups:
group_id = group.get("group_id", "")
matches = known_map.get(group_id, [])
raw_result = heuristic_classification(group, evidence, matches)
final_group = finalize_classification(raw_result, matches, args.admin_handle)
final_group.update(
{
"group_id": group_id,
"group_signature": group.get("signature", ""),
"group_size": len(group.get("cases", [])),
}
)
per_group_results.append(final_group)
category_counts[final_group["classification"]] = category_counts.get(final_group["classification"], 0) + 1

has_regression = any(item.get("classification") == CODE_REGRESSION for item in per_group_results)
summary = {
"group_count": len(groups),
"category_counts": {k: v for k, v in category_counts.items() if v > 0},
"has_code_regression_group": has_regression,
}

failure_count = sum(len(group.get("cases", [])) for group in groups)
final = {
"per_group_results": per_group_results,
"summary": summary,
"evidence_bundle": evidence,
"source_commit": payload.get("build", {}).get("source_commit", ""),
"pr_number": payload.get("build", {}).get("pr_number", ""),
"group_count": len(groups),
"failure_count": failure_count,
"ci_admin_handle": args.admin_handle,
}

write_json(args.output, final)

emit_pipeline_variable("CLASSIFICATION", CODE_REGRESSION if has_regression else "Non-Regression")
emit_pipeline_variable("HANDLING_ACTION", "grouped_routing")

print(
f"classify: groups={len(groups)} failures={failure_count} "
f"regression_groups={summary['category_counts'].get(CODE_REGRESSION, 0)}"
)
print(f"classify: result written to {args.output}")


if __name__ == "__main__":
main()
Loading
Loading