183 lines
6.0 KiB
Bash
Executable File
183 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
QUALITY_FILE="${QUALITY_FILE:-$ROOT_DIR/training_data/lora/mcp_call_quality.jsonl}"
|
|
GOOD_FILE="${GOOD_FILE:-$ROOT_DIR/training_data/lora/mcp_calls_good.jsonl}"
|
|
BAD_FILE="${BAD_FILE:-$ROOT_DIR/training_data/lora/mcp_calls_bad.jsonl}"
|
|
|
|
python3 - "$QUALITY_FILE" "$GOOD_FILE" "$BAD_FILE" <<'PY'
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from collections import Counter
|
|
|
|
quality=Path(sys.argv[1])
|
|
good=Path(sys.argv[2])
|
|
bad=Path(sys.argv[3])
|
|
|
|
for p in (quality,good,bad):
|
|
if not p.exists():
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text('',encoding='utf-8')
|
|
|
|
schema_re = re.compile(
|
|
r"invalid_action|invalid[_\s-]?json|json schema|schema|missing_required_args|"
|
|
r"schema_boundary_violation|task_id_missing|taskitems_missing_or_invalid|"
|
|
r"task_entry_invalid|no_sections_found|invalid[_\s-]?arg|field.+missing",
|
|
re.IGNORECASE
|
|
)
|
|
timeout_re = re.compile(
|
|
r"timeout|timed out|max_turns_reached|no_final_action|deadline exceeded|executor timeout",
|
|
re.IGNORECASE
|
|
)
|
|
transport_re = re.compile(
|
|
r"curl:\s*\(7\)|failed to connect|connection refused|no route to host|network unreachable|dns",
|
|
re.IGNORECASE
|
|
)
|
|
policy_re = re.compile(
|
|
r"permission denied|forbidden|unauthorized|blocked_by_policy|policy|scope|out[-_\s]?of[-_\s]?scope",
|
|
re.IGNORECASE
|
|
)
|
|
|
|
def infer_label(rec):
|
|
label = rec.get("label")
|
|
if label in ("good", "bad"):
|
|
return label
|
|
src = rec.get("source")
|
|
if src == "pipeline_run":
|
|
return "good"
|
|
if src in ("adapter_loop", "mcp_tool_response", "stage_labeled", "synthetic_counterfactual"):
|
|
return "bad"
|
|
payload = rec.get("payload", {})
|
|
if isinstance(payload, dict):
|
|
if payload.get("stage") == "tool_execution" and payload.get("result") == "success":
|
|
return "good"
|
|
return None
|
|
|
|
def infer_tool_call_success(rec, label):
|
|
payload = rec.get("payload", {})
|
|
if isinstance(payload, dict):
|
|
if payload.get("stage") == "tool_execution":
|
|
if payload.get("result") == "success":
|
|
return True
|
|
if payload.get("result") in ("error", "failed", "failure"):
|
|
return False
|
|
if label == "good":
|
|
return True
|
|
if label == "bad":
|
|
return False
|
|
return None
|
|
|
|
def classify_failure(rec, tool_call_success):
|
|
if tool_call_success is True:
|
|
return None
|
|
blob = " ".join(
|
|
str(x) for x in [
|
|
rec.get("malformed_kind", ""),
|
|
rec.get("error", ""),
|
|
rec.get("failure_reason", ""),
|
|
rec.get("failure_detail", ""),
|
|
json.dumps(rec.get("payload", {}), ensure_ascii=False)
|
|
]
|
|
)
|
|
if transport_re.search(blob):
|
|
return "transport"
|
|
if timeout_re.search(blob):
|
|
return "timeout"
|
|
if policy_re.search(blob):
|
|
return "policy"
|
|
if schema_re.search(blob):
|
|
return "schema"
|
|
return "logic"
|
|
|
|
def classify_recovery_pattern(rec, tool_call_success):
|
|
if tool_call_success is True:
|
|
return "none"
|
|
mk = " ".join(
|
|
str(x) for x in [
|
|
rec.get("malformed_kind", ""),
|
|
rec.get("error", ""),
|
|
rec.get("failure_reason", ""),
|
|
rec.get("failure_detail", ""),
|
|
json.dumps(rec.get("payload", {}), ensure_ascii=False)
|
|
]
|
|
).lower()
|
|
if "retry" in mk:
|
|
return "retry"
|
|
if any(x in mk for x in [
|
|
"missing_required_args", "schema_boundary_violation", "task_id_missing",
|
|
"taskitems_missing_or_invalid", "task_entry_invalid", "no_sections_found",
|
|
"invalid_action_format", "invalid_action"
|
|
]):
|
|
return "reformulate_args"
|
|
if any(x in mk for x in [
|
|
"wrong_tool_selection", "recovery_path_misroute", "dependency_order_violation",
|
|
"overfetch_context"
|
|
]):
|
|
return "choose_different_tool"
|
|
if any(x in mk for x in [
|
|
"premature_finalize", "non_terminal_loop", "max_turns_reached", "no_final_action", "blocked"
|
|
]):
|
|
return "abort"
|
|
return "unknown"
|
|
|
|
def classify_failure_terminality(recovery_pattern):
|
|
if recovery_pattern == "none":
|
|
return None
|
|
if recovery_pattern in ("retry", "reformulate_args", "choose_different_tool"):
|
|
return "recoverable"
|
|
if recovery_pattern == "abort":
|
|
return "terminal"
|
|
return "unknown"
|
|
|
|
records=[]
|
|
for ln in quality.read_text(encoding='utf-8',errors='ignore').splitlines():
|
|
ln=ln.strip()
|
|
if not ln:
|
|
continue
|
|
try:
|
|
rec=json.loads(ln)
|
|
except Exception:
|
|
continue
|
|
|
|
label = infer_label(rec)
|
|
if label is None:
|
|
continue
|
|
rec["label"] = label
|
|
if label == "good":
|
|
rec["malformed_kind"] = None
|
|
|
|
tool_call_success = infer_tool_call_success(rec, label)
|
|
rec["tool_call_success"] = tool_call_success
|
|
rec["failure_class"] = classify_failure(rec, tool_call_success)
|
|
rec["recovery_pattern"] = classify_recovery_pattern(rec, tool_call_success)
|
|
rec["failure_terminality"] = classify_failure_terminality(rec["recovery_pattern"])
|
|
rec["quality_schema_version"] = 2
|
|
records.append(rec)
|
|
|
|
# Deduplicate by serialized record.
|
|
seen=set(); dedup=[]
|
|
for r in records:
|
|
s=json.dumps(r,sort_keys=True,separators=(',',':'))
|
|
if s in seen:
|
|
continue
|
|
seen.add(s)
|
|
dedup.append(r)
|
|
|
|
quality.write_text(''.join(json.dumps(r,separators=(',',':'))+'\n' for r in dedup),encoding='utf-8')
|
|
|
|
good_rows=[r for r in dedup if r.get('label')=='good']
|
|
bad_rows=[r for r in dedup if r.get('label')=='bad']
|
|
|
|
good.write_text(''.join(json.dumps(r,separators=(',',':'))+'\n' for r in good_rows),encoding='utf-8')
|
|
bad.write_text(''.join(json.dumps(r,separators=(',',':'))+'\n' for r in bad_rows),encoding='utf-8')
|
|
|
|
fc = Counter((r.get("failure_class") or "none") for r in dedup)
|
|
rp = Counter((r.get("recovery_pattern") or "none") for r in dedup)
|
|
print(f'reclassified total={len(dedup)} good={len(good_rows)} bad={len(bad_rows)}')
|
|
print("failure_class_counts", dict(fc))
|
|
print("recovery_pattern_counts", dict(rp))
|
|
PY
|