inspect-eval-log-analysis
$
npx mdskill add microsoft/ACESEvals/inspect-eval-log-analysisTo parse and analyze inspect_ai `.eval` log files, follow this process:
SKILL.md
.github/skills/inspect-eval-log-analysisView on GitHub ↗
---
name: inspect-eval-log-analysis
description: Guide for parsing and analyzing inspect_ai .eval log files. Use this when asked to interpret eval results, extract tool calls, find scores, or investigate agent behavior from .eval logs.
---
To parse and analyze inspect_ai `.eval` log files, follow this process:
## 1. Understanding the `.eval` File Format
`.eval` files are **ZIP archives** (not plain text). They contain structured JSON:
```
<hash>.eval (ZIP)
├── header.json # Eval metadata
├── _journal/
│ ├── start.json # Eval config, plan, model info
│ └── summaries/
│ └── 1.json # Per-epoch summary
├── samples/
│ └── <task_id>_epoch_1.json # Full sample data per task
├── summaries.json # Aggregated score summaries
└── reductions.json # Score reduction results
```
**Never try to read `.eval` files as plain text.** Always use `zipfile`:
```python
import json, zipfile
with zipfile.ZipFile("logs/<file>.eval", "r") as zf:
print(zf.namelist()) # See all files inside
```
## 2. Extract Sample Data (Messages, Tool Calls, Scores)
The richest data is in the `samples/` JSON files. Each sample contains the full conversation:
```python
import json, zipfile
with zipfile.ZipFile("logs/<file>.eval", "r") as zf:
for name in zf.namelist():
if not name.startswith("samples/"):
continue
sample = json.loads(zf.read(name))
# Key fields in a sample:
# sample["id"] — task ID
# sample["messages"] — full message history (list of dicts)
# sample["scores"] — dict of scorer_name → score result
# sample["metadata"] — task metadata
```
## 3. Analyze Tool Calls from Messages
Tool calls are embedded in assistant messages. The structure varies slightly between Completions API and Responses API, but inspect_ai normalizes them:
```python
import json, zipfile
def extract_tool_calls(eval_path):
"""Extract all tool calls from an eval log."""
results = []
with zipfile.ZipFile(eval_path, "r") as zf:
for name in zf.namelist():
if not name.startswith("samples/"):
continue
sample = json.loads(zf.read(name))
for msg in sample.get("messages", []):
if not isinstance(msg, dict):
continue
for tc in msg.get("tool_calls", []):
if not isinstance(tc, dict):
continue
results.append({
"id": tc.get("id"),
"function": tc.get("function"),
"arguments": tc.get("arguments", {}),
"type": tc.get("type"),
})
return results
# Usage
calls = extract_tool_calls("logs/<file>.eval")
for c in calls:
fn = c["function"]
args = c["arguments"]
if isinstance(args, dict):
cmd = args.get("cmd", args.get("code", ""))
print(f"{fn}: {cmd[:80]}")
```
## 4. Analyze Scores
Scores are stored in the `scores` field of each sample:
```python
import json, zipfile
with zipfile.ZipFile("logs/<file>.eval", "r") as zf:
for name in zf.namelist():
if not name.startswith("samples/"):
continue
sample = json.loads(zf.read(name))
task_id = sample.get("id", name)
scores = sample.get("scores", {})
for scorer_name, score_data in scores.items():
value = score_data.get("value")
explanation = score_data.get("explanation", "")
print(f"{task_id} | {scorer_name}: {value}")
if explanation:
print(f" → {explanation[:200]}")
```
## 5. Scan for Specific Patterns Across Runs
Useful for tracking issues like model garbage output across multiple eval runs:
```python
import json, zipfile, os
log_dir = "logs"
evals = sorted([f for f in os.listdir(log_dir)
if "<domain>" in f and f.endswith(".eval")])
for logname in evals:
log_path = os.path.join(log_dir, logname)
matches = 0
total = 0
with zipfile.ZipFile(log_path, "r") as zf:
for name in zf.namelist():
if not name.startswith("samples/"):
continue
sample = json.loads(zf.read(name))
for msg in sample.get("messages", []):
if not isinstance(msg, dict):
continue
for tc in msg.get("tool_calls", []):
if not isinstance(tc, dict):
continue
total += 1
args = tc.get("arguments", {})
if isinstance(args, dict):
cmd = str(args.get("cmd", ""))
# Example: detect trailing garbage '}'
if cmd.rstrip().endswith("}"):
matches += 1
print(f"{logname}: {matches}/{total} matched")
```
## 6. Extract the Model Call Details
For deeper debugging, the `_journal/start.json` contains the eval plan and model configuration:
```python
import json, zipfile
with zipfile.ZipFile("logs/<file>.eval", "r") as zf:
start = json.loads(zf.read("_journal/start.json"))
# Eval metadata
eval_info = start.get("eval", {})
print(f"Model: {eval_info.get('model')}")
print(f"Dataset: {eval_info.get('dataset', {}).get('name')}")
print(f"Created: {eval_info.get('created')}")
# Plan details
plan = start.get("plan", {})
print(f"Solver: {plan.get('solver', {}).get('name')}")
```
## 7. Read the Aggregated Summaries
For a quick overview without parsing individual samples:
```python
import json, zipfile
with zipfile.ZipFile("logs/<file>.eval", "r") as zf:
summaries = json.loads(zf.read("summaries.json"))
# summaries contains per-scorer aggregated stats (mean, stderr, etc.)
print(json.dumps(summaries, indent=2)[:2000])
```
## 8. One-Liner for Quick Score Check
```bash
# List all recent eval logs with scores
python3 -c "
import json, zipfile, os
for f in sorted(os.listdir('logs'))[-5:]:
if not f.endswith('.eval'): continue
with zipfile.ZipFile(f'logs/{f}','r') as zf:
for n in zf.namelist():
if not n.startswith('samples/'): continue
s = json.loads(zf.read(n))
scores = {k: v.get('value') for k, v in s.get('scores', {}).items()}
print(f'{f}: {s.get(\"id\",\"?\")} → {scores}')
"
```