Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

395 lines
14 KiB
Python

"""
MINT Evaluator
Evaluates agent answers against ground truth using metrics that mirror the
upstream MINT graders:
* ``exact_match`` : normalized string equality (with light fallback).
* ``numeric`` : floats within 2% relative tolerance.
* ``code_test`` : run candidate code + test suite via the upstream
``check_correctness`` sandbox (HumanEval / MBPP).
* ``partial_match`` : substring / token overlap (HotpotQA).
* ``semantic`` : Jaccard token overlap.
* ``multiple_choice`` : MMLU letter-or-content match (upstream
``MultipleChoiceTask.success``).
* ``theoremqa`` : the TheoremQA grader (numbers / lists / bool).
"""
from __future__ import annotations
import logging
import re
from typing import Optional
from benchmarks.mint.types import (
MINTTask,
MINTTrajectory,
MINTResult,
)
logger = logging.getLogger(__name__)
class MINTEvaluator:
"""Evaluate MINT task solutions."""
NUMBER_PATTERN = r"-?\d+(?:\.\d+)?"
def __init__(self, strict: bool = False) -> None:
self.strict = strict
# ------------------------------------------------------------------
# Trajectory-level entry point
# ------------------------------------------------------------------
def evaluate_trajectory(
self,
task: MINTTask,
trajectory: MINTTrajectory,
) -> MINTResult:
predicted = trajectory.final_answer or ""
success, score, details = self.evaluate(
predicted=predicted,
expected=task.ground_truth,
metric=task.evaluation_metric,
task=task,
)
latency_ms = max(0.0, trajectory.end_time_ms - trajectory.start_time_ms)
assistant_turns = [t for t in trajectory.turns if t.turn_type.value == "assistant"]
# Per-turn cumulative success. The trajectory already records each
# proposed answer; we re-grade each one so the evaluator stays the
# single source of truth for "is this correct".
per_turn = self._grade_per_turn(task, trajectory)
return MINTResult(
task_id=task.id,
subtask=task.subtask,
trajectory=trajectory,
success=success,
turns_used=len(assistant_turns),
tool_uses=trajectory.num_tool_uses,
feedback_turns=trajectory.num_feedback_turns,
latency_ms=latency_ms,
token_usage=trajectory.total_tokens,
score=score,
evaluation_details=details,
cumulative_success_per_turn=per_turn,
)
def _grade_per_turn(
self, task: MINTTask, trajectory: MINTTrajectory
) -> list[bool]:
"""Cumulative success flags, one entry per assistant turn."""
flags: list[bool] = []
any_correct = False
for answer in trajectory.per_turn_answers:
if answer is None or any_correct:
flags.append(any_correct)
continue
ok, _, _ = self.evaluate(
predicted=answer,
expected=task.ground_truth,
metric=task.evaluation_metric,
task=task,
)
any_correct = any_correct or ok
flags.append(any_correct)
return flags
# ------------------------------------------------------------------
# Single-answer entry point
# ------------------------------------------------------------------
def evaluate(
self,
predicted: str,
expected: str,
metric: str = "exact_match",
task: Optional[MINTTask] = None,
) -> tuple[bool, float, dict[str, str | int | float | bool]]:
details: dict[str, str | int | float | bool] = {
"metric": metric,
"predicted": str(predicted)[:200],
"expected": str(expected)[:200],
}
if not predicted:
details["error"] = "No answer provided"
return False, 0.0, details
if metric == "exact_match":
success, score = self._exact_match(predicted, expected)
elif metric == "numeric":
success, score = self._numeric_match(predicted, expected)
elif metric in {"code_test", "code_output"}:
success, score = self._code_test_match(predicted, expected, task)
elif metric == "partial_match":
success, score = self._partial_match(predicted, expected)
elif metric == "semantic":
success, score = self._semantic_match(predicted, expected)
elif metric == "multiple_choice":
success, score = self._multiple_choice_match(predicted, expected)
elif metric == "theoremqa":
success, score = self._theoremqa_match(predicted, expected, task)
else:
logger.warning(
"[MINTEvaluator] Unknown metric %s; falling back to exact_match",
metric,
)
success, score = self._exact_match(predicted, expected)
details["success"] = success
details["score"] = score
return success, score, details
# ------------------------------------------------------------------
# Metric implementations
# ------------------------------------------------------------------
def _exact_match(self, predicted: str, expected: str) -> tuple[bool, float]:
pred = self._normalize(predicted)
exp = self._normalize(expected)
if pred == exp:
return True, 1.0
return False, self._string_similarity(pred, exp)
def _numeric_match(
self,
predicted: str,
expected: str,
tolerance: float = 0.02,
) -> tuple[bool, float]:
try:
pred_nums = re.findall(self.NUMBER_PATTERN, predicted)
exp_nums = re.findall(self.NUMBER_PATTERN, expected)
if not pred_nums:
return False, 0.0
if not exp_nums:
return self._exact_match(predicted, expected)
pred_num = float(pred_nums[-1])
exp_num = float(exp_nums[-1])
if self.strict:
tolerance = 0.0
if pred_num == exp_num:
return True, 1.0
if not self.strict and round(pred_num, 2) == round(exp_num, 2):
return True, 1.0
if exp_num == 0:
return (abs(pred_num) < tolerance, max(0.0, 1 - abs(pred_num)))
rel = abs(pred_num - exp_num) / abs(exp_num)
if rel <= tolerance:
return True, 1.0
return False, max(0.0, 1 - rel)
except (ValueError, IndexError, ZeroDivisionError):
return self._exact_match(predicted, expected)
def _code_test_match(
self,
predicted: str,
expected: str,
task: Optional[MINTTask],
) -> tuple[bool, float]:
"""Execute candidate code against the upstream test suite.
``expected`` is the test code (HumanEval / MBPP convention); we
delegate to the upstream sandbox so we keep parity with the paper.
"""
try:
from benchmarks.mint.upstream.mint.utils.exec import check_correctness
except Exception as exc: # ImportError or signal-unsupported on Win.
logger.warning(
"[MINTEvaluator] Upstream exec sandbox unavailable (%s); "
"falling back to exact match on the candidate code.",
exc,
)
return self._exact_match(predicted, expected)
candidate = self._extract_code_block(predicted)
# MBPP packs its tests differently — upstream pulls them from the
# task's ``test_list`` rather than the reference field. We respect
# whichever lives on the task metadata if present.
test_code = expected
if task is not None and "test_list" in task.metadata:
import json as _json
try:
test_code = "\n".join(_json.loads(task.metadata["test_list"]))
except Exception:
test_code = expected
try:
result = check_correctness(
solution_code=candidate,
test_code=test_code,
timeout=10,
)
return bool(result.get("success")), 1.0 if result.get("success") else 0.0
except Exception as exc:
logger.warning("[MINTEvaluator] check_correctness raised %s", exc)
return self._exact_match(predicted, expected)
def _partial_match(self, predicted: str, expected: str) -> tuple[bool, float]:
pred = self._normalize(predicted)
exp = self._normalize(expected)
if not pred or not exp:
return False, 0.0
if exp in pred or pred in exp:
return True, 1.0
pred_tokens = set(pred.split(","))
exp_tokens = set(exp.split(","))
if pred_tokens and exp_tokens:
overlap = len(pred_tokens & exp_tokens)
union = len(pred_tokens | exp_tokens)
if union and overlap / union >= 0.8:
return True, overlap / union
sim = self._string_similarity(pred, exp)
return sim >= 0.9, sim
def _semantic_match(self, predicted: str, expected: str) -> tuple[bool, float]:
pred = set(self._normalize(predicted).split())
exp = set(self._normalize(expected).split())
if not exp:
return False, 0.0
union = len(pred | exp)
if union == 0:
return False, 0.0
sim = len(pred & exp) / union
return sim >= 0.7, sim
def _multiple_choice_match(
self, predicted: str, expected: str
) -> tuple[bool, float]:
"""Letter-or-content match for MMLU-style tasks."""
pred = predicted.lower().strip()
exp = expected.lower().strip()
if not pred:
return False, 0.0
# Match letter answers like "a)", "(b)", "answer: c"
for letter in "abcdefghijklmnopqrstuvwxyz":
if (
pred == letter
or re.search(rf"\b{letter}\b\s*\)", pred)
or pred.endswith(f" {letter}")
or pred.startswith(f"{letter})")
or f"answer: {letter}" in pred
or f"answer is {letter}" in pred
):
return (letter == exp, 1.0 if letter == exp else 0.0)
# Fallback: substring containment.
if exp in pred:
return True, 1.0
return False, 0.0
def _theoremqa_match(
self,
predicted: str,
expected: str,
task: Optional[MINTTask],
) -> tuple[bool, float]:
"""Defer to the upstream TheoremQA grader when possible."""
try:
from benchmarks.mint.upstream.mint.tasks.reasoning import TheoremqaTask
except Exception as exc:
logger.warning(
"[MINTEvaluator] TheoremQA grader unavailable (%s)", exc
)
return self._numeric_match(predicted, expected)
answer_type = "float"
if task is not None and task.metadata.get("answer_type"):
answer_type = str(task.metadata["answer_type"])
try:
# Upstream TheoremqaTask wants the reference as the typed object,
# not a string. We attempt to JSON-parse the stored reference.
import json as _json
try:
ref = _json.loads(expected)
except Exception:
ref = expected
grader = TheoremqaTask(
id=task.id if task else "theoremqa",
prompt=task.initial_prompt if task else "",
reference=ref,
answer_type=answer_type,
)
ok = bool(grader.success(predicted))
return ok, 1.0 if ok else 0.0
except Exception as exc:
logger.warning("[MINTEvaluator] TheoremQA grader raised %s", exc)
return self._numeric_match(predicted, expected)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _normalize(self, text: str) -> str:
if not text:
return ""
out = str(text).strip().lower()
out = re.sub(r"[.,!?;:]+$", "", out)
out = re.sub(r"\s+", " ", out)
for prefix in ("the answer is", "answer:", "result:", "therefore", "thus"):
if out.startswith(prefix):
out = out[len(prefix):].strip()
return out
def _string_similarity(self, a: str, b: str) -> float:
if not a or not b:
return 0.0
if a == b:
return 1.0
matches = sum(1 for i, c in enumerate(a) if i < len(b) and b[i] == c)
return matches / max(len(a), len(b))
def _extract_code_block(self, text: str) -> str:
"""Pull the first fenced code block out of an LLM response, else return text."""
match = re.search(r"```(?:python)?\s*(.*?)```", text, flags=re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return text.strip()
class BatchEvaluator:
"""Evaluate multiple trajectories and aggregate results."""
def __init__(self, evaluator: Optional[MINTEvaluator] = None) -> None:
self.evaluator = evaluator or MINTEvaluator()
def evaluate_batch(
self,
tasks: list[MINTTask],
trajectories: list[MINTTrajectory],
) -> list[MINTResult]:
return [
self.evaluator.evaluate_trajectory(task, traj)
for task, traj in zip(tasks, trajectories)
]
def aggregate_results(self, results: list[MINTResult]) -> dict[str, float | int]:
if not results:
return {
"total": 0,
"passed": 0,
"failed": 0,
"success_rate": 0.0,
"avg_score": 0.0,
"avg_turns": 0.0,
"avg_tool_uses": 0.0,
}
total = len(results)
passed = sum(1 for r in results if r.success)
return {
"total": total,
"passed": passed,
"failed": total - passed,
"success_rate": passed / total if total else 0.0,
"avg_score": sum(r.score for r in results) / total,
"avg_turns": sum(r.turns_used for r in results) / total,
"avg_tool_uses": sum(r.tool_uses for r in results) / total,
"avg_feedback_turns": sum(r.feedback_turns for r in results) / total,
"avg_latency_ms": sum(r.latency_ms for r in results) / total,
}