mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 09:15:22 -04:00
510c68f7be
Fixes #2441 `_format_table` used `r.get("solve_rate") or "n/a"` which treats legitimate `0`/`0.0` values as falsy, displaying "n/a" instead of the actual zero score in the CI evals summary. The sort key on line 164 had the same pattern. Replaced all `or "n/a"` / `or 0.0` truthiness guards with explicit `is None` checks, consistent with the footer detection already in the same file (lines 191-196). **Before:** A model scoring 0% on solve_rate shows "n/a" (misrepresented as "no data") **After:** A model scoring 0% on solve_rate shows `0.0` (correct) ### How I verified ```python >>> 0.0 or "n/a" # old — returns "n/a" (wrong) >>> "n/a" if 0.0 is None else 0.0 # new — returns 0.0 (correct) >>> "n/a" if None is None else None # new — returns "n/a" (correct) ``` Confirmed that the producers (`_solve_rate`, `_micro_step_ratio`, `_micro_tool_call_ratio` in `pytest_reporter.py`) return `float | None`, where `None` means "no eligible tests" and `0.0` means "all eligible tests failed". Co-authored-by: Mason Daugherty <mason@langchain.dev>
260 lines
7.7 KiB
Python
260 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import glob
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from tabulate import tabulate
|
|
|
|
|
|
def _format_table(
|
|
rows: list[dict[str, object]], headers: list[str]
|
|
) -> list[list[object]]:
|
|
"""Build tabulate-ready rows from report dicts."""
|
|
return [
|
|
[
|
|
str(r.get("model", "")),
|
|
r.get("passed", 0),
|
|
r.get("failed", 0),
|
|
r.get("skipped", 0),
|
|
r.get("total", 0),
|
|
r.get("correctness", 0.0),
|
|
"n/a" if r.get("solve_rate") is None else r.get("solve_rate"),
|
|
"n/a" if r.get("step_ratio") is None else r.get("step_ratio"),
|
|
"n/a" if r.get("tool_call_ratio") is None else r.get("tool_call_ratio"),
|
|
r.get("median_duration_s", 0.0),
|
|
]
|
|
for r in rows
|
|
]
|
|
|
|
|
|
_COLALIGN = (
|
|
"left",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
"right",
|
|
)
|
|
|
|
_HEADERS = [
|
|
"model",
|
|
"passed",
|
|
"failed",
|
|
"skipped",
|
|
"total",
|
|
"correctness",
|
|
"solve_rate",
|
|
"step_ratio",
|
|
"tool_call_ratio",
|
|
"median_duration_s",
|
|
]
|
|
|
|
|
|
_CATEGORIES_JSON = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "libs"
|
|
/ "evals"
|
|
/ "deepagents_evals"
|
|
/ "categories.json"
|
|
)
|
|
|
|
|
|
def _load_category_labels() -> dict[str, str]:
|
|
"""Load human-readable category labels from `categories.json`.
|
|
|
|
Returns:
|
|
Mapping of category name to display label, or empty dict on failure.
|
|
"""
|
|
try:
|
|
return json.loads(_CATEGORIES_JSON.read_text(encoding="utf-8"))["labels"]
|
|
except (FileNotFoundError, json.JSONDecodeError, KeyError) as exc:
|
|
print(
|
|
f"warning: could not load category labels from {_CATEGORIES_JSON}: {exc}",
|
|
file=sys.stderr,
|
|
)
|
|
return {}
|
|
|
|
|
|
def _build_category_table(rows: list[dict[str, object]]) -> list[str]:
|
|
"""Build a per-category scores table from report rows.
|
|
|
|
Returns a single-element list containing the rendered Markdown table
|
|
string, or an empty list when no category data is present.
|
|
|
|
Args:
|
|
rows: Report row dicts, each expected to contain a `category_scores`
|
|
mapping and a `model` string.
|
|
"""
|
|
# Collect all categories across all models (preserving insertion order).
|
|
all_cats: list[str] = list(
|
|
dict.fromkeys(cat for r in rows for cat in (r.get("category_scores") or {}))
|
|
)
|
|
|
|
if not all_cats:
|
|
return []
|
|
|
|
labels = _load_category_labels()
|
|
headers = ["model", *[labels.get(c, c) for c in all_cats]]
|
|
table_rows: list[list[object]] = []
|
|
for r in rows:
|
|
scores = r.get("category_scores") or {}
|
|
table_rows.append(
|
|
[
|
|
str(r.get("model", "")),
|
|
*[scores.get(c, "—") for c in all_cats],
|
|
]
|
|
)
|
|
|
|
colalign = ("left", *("right" for _ in all_cats))
|
|
return [tabulate(table_rows, headers=headers, tablefmt="github", colalign=colalign)]
|
|
|
|
|
|
def main() -> None:
|
|
"""Generate an aggregated report."""
|
|
report_files = sorted(
|
|
glob.glob("evals_artifacts/**/evals_report.json", recursive=True)
|
|
)
|
|
|
|
rows: list[dict[str, object]] = []
|
|
for file in report_files:
|
|
payload = json.loads(Path(file).read_text(encoding="utf-8"))
|
|
rows.append(payload)
|
|
|
|
# --- JSON artifact for offline analysis ---
|
|
summary_json_path = Path("evals_summary.json")
|
|
summary_json_path.write_text(
|
|
json.dumps(rows, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
)
|
|
|
|
# --- Table 1: grouped by provider, then correctness desc ---
|
|
by_provider = sorted(
|
|
rows,
|
|
key=lambda r: (
|
|
str(r.get("model", "")).split(":")[0],
|
|
-float(r.get("correctness", 0.0)),
|
|
),
|
|
)
|
|
|
|
lines: list[str] = []
|
|
lines.append("## Evals summary")
|
|
lines.append("")
|
|
lines.append(
|
|
"> These are the final aggregated results across all models after"
|
|
" every eval job has completed."
|
|
)
|
|
lines.append("")
|
|
|
|
table_rows = _format_table(by_provider, _HEADERS)
|
|
if table_rows:
|
|
lines.append(
|
|
tabulate(
|
|
table_rows, headers=_HEADERS, tablefmt="github", colalign=_COLALIGN
|
|
)
|
|
)
|
|
else:
|
|
lines.append("_No eval artifacts found._")
|
|
|
|
# --- Table 2: ranked by correctness desc, then solve_rate desc ---
|
|
by_correctness = sorted(
|
|
rows,
|
|
key=lambda r: (
|
|
-float(r.get("correctness", 0.0)),
|
|
-float(0.0 if r.get("solve_rate") is None else r.get("solve_rate")),
|
|
),
|
|
)
|
|
|
|
lines.append("")
|
|
lines.append("## Ranked by correctness / solve rate")
|
|
lines.append("")
|
|
|
|
ranked_rows = _format_table(by_correctness, _HEADERS)
|
|
if ranked_rows:
|
|
lines.append(
|
|
tabulate(
|
|
ranked_rows, headers=_HEADERS, tablefmt="github", colalign=_COLALIGN
|
|
)
|
|
)
|
|
else:
|
|
lines.append("_No eval artifacts found._")
|
|
|
|
# --- Table 3: per-category scores ---
|
|
cat_table = _build_category_table(rows)
|
|
if cat_table:
|
|
lines.append("")
|
|
lines.append("## Per-category correctness")
|
|
lines.append("")
|
|
lines.extend(cat_table)
|
|
|
|
# --- Footer for n/a metrics ---
|
|
has_na = any(
|
|
r.get("solve_rate") is None
|
|
and r.get("step_ratio") is None
|
|
and r.get("tool_call_ratio") is None
|
|
for r in rows
|
|
)
|
|
if has_na:
|
|
lines.append("")
|
|
lines.append(
|
|
"> **N/A** — `solve_rate`, `step_ratio`, and `tool_call_ratio` are only"
|
|
" reported for eval categories whose tests specify expected step/tool-call"
|
|
" counts. Categories that use custom assertions (e.g. summarization) do not"
|
|
" currently produce these metrics."
|
|
)
|
|
|
|
# --- Reference links ---
|
|
lines.append("")
|
|
lines.append(
|
|
"📚 [Eval Catalog](https://github.com/langchain-ai/deepagents/blob/main/libs/evals/EVAL_CATALOG.md)"
|
|
" | [Model Groups](https://github.com/langchain-ai/deepagents/blob/main/libs/evals/MODEL_GROUPS.md)"
|
|
)
|
|
|
|
# --- LangSmith experiment links ---
|
|
experiment_entries: list[
|
|
tuple[str, str, str, str]
|
|
] = [] # (model, name, url, public_url)
|
|
for r in rows:
|
|
model = str(r.get("model", ""))
|
|
# Prefer rich experiment_links (name + url); fall back to bare experiment_urls
|
|
# for older reports that only have the URL list.
|
|
raw_links = r.get("experiment_links") or []
|
|
if isinstance(raw_links, list) and raw_links:
|
|
for link in raw_links:
|
|
if isinstance(link, dict):
|
|
name = str(link.get("name", ""))
|
|
url = str(link.get("url", ""))
|
|
public_url = str(link.get("public_url", ""))
|
|
if url:
|
|
experiment_entries.append((model, name or url, url, public_url))
|
|
else:
|
|
raw_urls = r.get("experiment_urls") or []
|
|
if isinstance(raw_urls, list):
|
|
for url in raw_urls:
|
|
experiment_entries.append((model, str(url), str(url), ""))
|
|
if experiment_entries:
|
|
lines.append("")
|
|
lines.append("## LangSmith experiments")
|
|
lines.append("")
|
|
for model, name, url, public_url in experiment_entries:
|
|
if public_url:
|
|
lines.append(
|
|
f"- **{model}**: [{name}]({public_url}) ([internal]({url}))"
|
|
)
|
|
else:
|
|
lines.append(f"- **{model}**: [{name}]({url})")
|
|
|
|
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
if summary_file:
|
|
Path(summary_file).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
print("\n".join(lines))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|