| name | inspect-ai |
| description | Analyze Inspect AI evaluation logs, understand EvalLog structure, extract samples, events, and scoring data using dataframes |
| user-invocable | false |
Inspect AI Log Analysis Reference
Use this knowledge when working with Inspect AI evaluation logs (.eval or .json files).
File Format
.eval files are binary (compressed) format containing JSON data. They are essentially zip archives. To read them programmatically, use the Inspect Python API.
Core Data Structures
EvalLog (Top-level)
class EvalLog:
version: int
status: str
eval: EvalSpec
plan: EvalPlan
results: EvalResults
stats: EvalStats
error: EvalError | None
samples: list[EvalSample]
reductions: list[EvalSampleReduction]
Always check log.status == "success" before analyzing results.
EvalSample (Per-sample data)
class EvalSample:
id: int | str
epoch: int
input: str | list[ChatMessage]
target: str | list[str]
choices: list[str] | None
messages: list[ChatMessage]
output: ModelOutput
scores: dict[str, Score] | None
events: list[Event]
store: dict[str, Any]
attachments: dict[str, str]
metadata: dict[str, Any]
sandbox: SandboxEnvironmentSpec | None
files: list[str] | None
setup: str | None
started_at: datetime | None
completed_at: datetime | None
total_time: float | None
working_time: float | None
model_usage: dict[str, ModelUsage]
error: EvalError | None
error_retries: list[EvalError] | None
limit: EvalSampleLimit | None
Event Types (for behavioral analysis)
Events are the core of behavioral analysis. Each sample has an events list containing:
Event = Union[
SampleInitEvent,
SampleLimitEvent,
SandboxEvent,
StateEvent,
StoreEvent,
ModelEvent,
ToolEvent,
ApprovalEvent,
InputEvent,
ScoreEvent,
ScoreEditEvent,
ErrorEvent,
LoggerEvent,
InfoEvent,
SpanBeginEvent,
SpanEndEvent,
StepEvent,
SubtaskEvent,
]
ModelEvent (LLM calls)
class ModelEvent:
event: "model"
model: str
input: list[ChatMessage]
tools: list[ToolInfo]
tool_choice: ToolChoice
config: GenerateConfig
output: ModelOutput
retries: int | None
error: str | None
cache: "read" | "write" | None
timestamp: datetime
completed: datetime | None
working_time: float | None
ToolEvent (Tool calls)
class ToolEvent:
event: "tool"
id: str
function: str
arguments: dict[str, JsonValue]
result: ToolResult
error: ToolCallError | None
truncated: tuple[int, int] | None
timestamp: datetime
completed: datetime | None
working_time: float | None
agent: str | None
failed: bool | None
Score
class Score:
value: float | str | int | bool | list
answer: str | None
explanation: str | None
metadata: dict[str, Any] | None
history: list[ScoreEdit]
Dataframe API (Primary Analysis Method)
The inspect_ai.analysis module provides functions to convert logs into Pandas dataframes.
evals_df() - One row per evaluation
from inspect_ai.analysis import evals_df
df = evals_df("logs")
df = evals_df(["path/to/file1.eval", "path/to/file2.eval"])
Default columns (~51):
eval_id - Unique evaluation identifier
log - URI of source file
task, task_version, task_file, task_arg_* - Task info
model, model_args, generate_config_* - Model info
status, error - Completion status
score_<scorer>_<metric> - All scores expanded as columns
samples_completed, samples_total
created, git_commit, tags, metadata_*
Pre-built column groups:
from inspect_ai.analysis import (
EvalInfo,
EvalTask,
EvalModel,
EvalDataset,
EvalConfig,
EvalResults,
EvalScores,
EvalColumns,
)
samples_df() - One row per sample
from inspect_ai.analysis import samples_df, SampleSummary, SampleScores, SampleMessages
df = samples_df("logs")
df = samples_df("logs", columns=SampleSummary + SampleScores)
df = samples_df("logs", columns=SampleSummary + SampleMessages)
SampleSummary columns (default, 12 columns):
sample_id - Globally unique identifier
eval_id - Links to evaluation
id, epoch - Sample ID within eval and epoch number
input, target - Task input and expected output
metadata_* - Expanded metadata dictionary
score_* - Score values only
model_usage - Token counts
total_time, working_time - Timing data
error, limit, retries - Failure info
log - Source file URI
SampleScores adds:
- Score answer, explanation, metadata
SampleMessages adds:
- Full message content (requires loading full sample)
messages_df() - One row per message
from inspect_ai.analysis import messages_df
df = messages_df("logs")
df = messages_df("logs", filter=["assistant"])
df = messages_df("logs", filter=["user", "assistant"])
df = messages_df("logs", filter=lambda msg: "error" in msg.content.lower())
Default columns:
sample_id, eval_id - Links to sample and evaluation
event_id - Unique message identifier
role - user, assistant, system, tool
content - Message text
source - Origin of message
tool_calls - Formatted function calls
tool_call_id, tool_call_function, tool_call_error
log - Source file URI
events_df() - One row per event
from inspect_ai.analysis import (
events_df,
EventInfo,
EventTiming,
ModelEventColumns,
ToolEventColumns,
)
df = events_df("logs", columns=EventInfo + EventTiming)
df = events_df("logs", columns=EventInfo + ToolEventColumns,
filter=lambda e: e.event == "tool")
EventInfo columns:
event_type - Type of event (model, tool, sandbox, etc.)
span_id - Span identifier for grouping
EventTiming columns:
timestamp - When event started
completed - When event finished
working_time - Active processing time
Joining Dataframes
Use eval_id and sample_id to join across dataframes:
merged = samples.merge(evals, on='eval_id')
merged = messages.merge(samples, on='sample_id')
import duckdb
con = duckdb.connect()
con.register('evals', evals_df("logs"))
con.register('samples', samples_df("logs"))
con.execute("""
SELECT e.model, AVG(s.score_accuracy)
FROM samples s JOIN evals e ON s.eval_id = e.eval_id
GROUP BY e.model
""")
Data Preparation Functions
from inspect_ai.analysis import prepare, model_info, task_info, frontier
df = prepare(df, model_info())
df = prepare(df, task_info({"gpqa_diamond": "GPQA Diamond"}))
df = prepare(df, frontier())
Low-level Log Reading API
from inspect_ai.log import (
read_eval_log,
read_eval_log_sample,
read_eval_log_samples,
read_eval_log_sample_summaries,
list_eval_logs,
)
log = read_eval_log("path/to/file.eval")
log = read_eval_log("path/to/file.eval", header_only=True)
for sample in read_eval_log_samples("path/to/file.eval"):
process(sample)
summaries = read_eval_log_sample_summaries("path/to/file.eval")
sample = read_eval_log_sample("path/to/file.eval", id="sample_id", epoch=1)
logs = list_eval_logs("./logs", recursive=True)
CLI Commands
inspect log list --json
inspect log list --status success
inspect log dump path/to/file.eval
inspect log convert file.json --to eval --output-dir ./converted
Common Analysis Patterns
QA Verification
evals = evals_df("logs")
failed = evals[evals['status'] != 'success']
samples = samples_df("logs")
errored = samples[samples['error'].notna()]
limited = samples[samples['limit'].notna()]
Behavioral Analysis
events = events_df("logs", columns=EventInfo + ToolEventColumns,
filter=lambda e: e.event == "tool")
tool_counts = events.groupby(['eval_id', 'function']).size()
messages = messages_df("logs")
msg_counts = messages.groupby(['eval_id', 'role']).size().unstack()
samples = samples_df("logs")
successful = samples[samples['score_accuracy'] == 1.0]
failed = samples[samples['score_accuracy'] == 0.0]
Cross-model Comparison
evals = evals_df("logs")
by_model = evals.groupby('model').agg({
'score_accuracy_mean': 'mean',
'samples_completed': 'sum'
})
Performance Tips
- Use
SampleSummary (default) for fast reads - only loads headers
- Use
parallel=True for large datasets: samples_df("logs", parallel=True)
- Use
header_only=True with read_eval_log() when you don't need samples
- Stream with
read_eval_log_samples() for memory-constrained environments
- Use
strict=False to get partial results: df, errors = evals_df("logs", strict=False)