code-review-env / analysis /__init__.py
ragavrida's picture
CodeReviewEnv v1.0 — OpenEnv-compliant submission
dbf5e96
Raw
History Blame Contribute Delete
11.7 kB
"""
Agent Capability Profiler
Analyzes trajectory data to characterize agent behavior along
research-relevant dimensions. Enables comparative analysis
across different agent architectures.
Research motivation:
Standard benchmarks report only aggregate scores. This profiler
enables fine-grained behavioral analysis:
- Exploration rate: is the agent stuck in a policy rut?
- Reward trajectory shape: does the agent learn within episodes?
- Action distribution: does the agent exploit trivial strategies?
- Severity calibration: how well-calibrated are predictions?
"""
import json
import os
import hashlib
import statistics
from typing import Dict, List, Tuple, Optional
from env.data_generator import SEVERITY_ORDER
class AgentProfiler:
"""
Analyzes trajectory data for research-grade agent characterization.
Use this to compare agent architectures beyond simple score tables.
Generate reports suitable for paper appendices.
"""
def load_trajectories(self, directory: str) -> List[Dict]:
"""
Load all JSONL trajectory files from directory.
Each file is one episode, each line is one (s, a, r, s') transition.
"""
trajectories = []
if not os.path.exists(directory):
return trajectories
for filename in sorted(os.listdir(directory)):
if filename.endswith(".jsonl"):
filepath = os.path.join(directory, filename)
episode = []
with open(filepath, "r") as f:
for line in f:
line = line.strip()
if line:
episode.append(json.loads(line))
if episode:
trajectories.append(episode)
return trajectories
def compute_exploration_rate(self, trajectories: List[List[Dict]]) -> float:
"""
Fraction of unique (state_hash, action_type) pairs visited.
Low exploration = agent stuck in policy rut (always same action).
High exploration = agent adapts to different states.
State hash uses PR title + author_experience to avoid
hash collisions on diff content.
"""
if not trajectories:
return 0.0
unique_pairs = set()
total_steps = 0
for episode in trajectories:
for transition in episode:
state = transition.get("state", {})
action = transition.get("action", {})
# Hash state on key semantic features
state_key = f"{state.get('pr_id', '')}_{state.get('title', '')}"
state_hash = hashlib.md5(state_key.encode()).hexdigest()[:8]
action_type = action.get("action_type", "unknown")
pair = (state_hash, action_type)
unique_pairs.add(pair)
total_steps += 1
if total_steps == 0:
return 0.0
return len(unique_pairs) / total_steps
def compute_reward_trajectory_shape(self, trajectories: List[List[Dict]]) -> Dict:
"""
Analyze how reward evolves across steps within episodes.
Returns:
slope: linear regression slope of reward over steps
variance: reward variance within episodes
monotonic_fraction: fraction of episodes with monotonically
increasing reward (in-context learning signal)
"""
if not trajectories:
return {"slope": 0.0, "variance": 0.0, "monotonic_fraction": 0.0}
all_slopes = []
all_variances = []
monotonic_count = 0
for episode in trajectories:
rewards = [t.get("reward", {}).get("value", 0.0) for t in episode]
if len(rewards) < 2:
continue
# Simple linear regression slope
n = len(rewards)
x_mean = (n - 1) / 2
y_mean = sum(rewards) / n
numerator = sum((i - x_mean) * (r - y_mean) for i, r in enumerate(rewards))
denominator = sum((i - x_mean) ** 2 for i in range(n))
slope = numerator / denominator if denominator != 0 else 0.0
all_slopes.append(slope)
# Variance
if len(rewards) > 1:
all_variances.append(statistics.variance(rewards))
# Monotonicity check
is_monotonic = all(rewards[i] <= rewards[i + 1] for i in range(len(rewards) - 1))
if is_monotonic:
monotonic_count += 1
n_episodes = len(trajectories)
return {
"slope": statistics.mean(all_slopes) if all_slopes else 0.0,
"variance": statistics.mean(all_variances) if all_variances else 0.0,
"monotonic_fraction": monotonic_count / n_episodes if n_episodes > 0 else 0.0,
}
def compute_action_distribution(self, trajectories: List[List[Dict]]) -> Dict:
"""
Distribution of action_types across all steps.
Reveals if agent exploits (e.g., always approve) or uses
the full action space. Uniform distribution over valid
actions suggests genuine exploration.
"""
counts: Dict[str, int] = {}
total = 0
for episode in trajectories:
for transition in episode:
action = transition.get("action", {})
action_type = action.get("action_type", "unknown")
counts[action_type] = counts.get(action_type, 0) + 1
total += 1
if total == 0:
return {}
return {k: {"count": v, "fraction": v / total} for k, v in sorted(counts.items())}
def compute_severity_calibration(self, trajectories: List[List[Dict]]) -> Dict:
"""
For easy task: calibration curve of predicted vs true severity.
Similar to probability calibration in classification literature.
Returns fraction of correct predictions per severity level.
"""
correct_by_severity: Dict[str, int] = {s: 0 for s in SEVERITY_ORDER}
total_by_severity: Dict[str, int] = {s: 0 for s in SEVERITY_ORDER}
for episode in trajectories:
for transition in episode:
action = transition.get("action", {})
if action.get("action_type") != "label_severity":
continue
predicted = action.get("severity", "none")
# Get true severity from reward reason
reason = transition.get("reward", {}).get("reason", "")
true_sev = None
if "Truth:" in reason:
parts = reason.split("Truth:")
if len(parts) > 1:
true_sev = parts[1].strip().split()[0].strip(",")
if true_sev and true_sev in total_by_severity:
total_by_severity[true_sev] += 1
if predicted == true_sev:
correct_by_severity[true_sev] += 1
calibration = {}
for sev in SEVERITY_ORDER:
total = total_by_severity[sev]
if total > 0:
calibration[sev] = {
"accuracy": correct_by_severity[sev] / total,
"total": total,
"correct": correct_by_severity[sev],
}
else:
calibration[sev] = {"accuracy": 0.0, "total": 0, "correct": 0}
return calibration
def compare_agents(self, agent_a_dir: str, agent_b_dir: str) -> Dict:
"""
Statistical comparison between two agents.
Uses Mann-Whitney U test for non-parametric comparison
and Cohen's d for effect size measurement.
"""
traj_a = self.load_trajectories(agent_a_dir)
traj_b = self.load_trajectories(agent_b_dir)
scores_a = [
statistics.mean([t.get("reward", {}).get("value", 0.0) for t in ep])
for ep in traj_a if ep
]
scores_b = [
statistics.mean([t.get("reward", {}).get("value", 0.0) for t in ep])
for ep in traj_b if ep
]
if not scores_a or not scores_b:
return {"error": "Insufficient trajectory data for comparison"}
mean_a = statistics.mean(scores_a)
mean_b = statistics.mean(scores_b)
std_a = statistics.stdev(scores_a) if len(scores_a) > 1 else 0.001
std_b = statistics.stdev(scores_b) if len(scores_b) > 1 else 0.001
# Cohen's d effect size
pooled_std = ((std_a ** 2 + std_b ** 2) / 2) ** 0.5
cohens_d = (mean_a - mean_b) / pooled_std if pooled_std > 0 else 0.0
# Mann-Whitney U (simplified — counts wins)
u_stat = 0
for sa in scores_a:
for sb in scores_b:
if sa > sb:
u_stat += 1
elif sa == sb:
u_stat += 0.5
n_a, n_b = len(scores_a), len(scores_b)
max_u = n_a * n_b
return {
"agent_a": {"mean": mean_a, "std": std_a, "n": n_a},
"agent_b": {"mean": mean_b, "std": std_b, "n": n_b},
"cohens_d": cohens_d,
"effect_interpretation": self._interpret_effect(abs(cohens_d)),
"mann_whitney_u": u_stat,
"u_normalized": u_stat / max_u if max_u > 0 else 0.0,
}
@staticmethod
def _interpret_effect(d: float) -> str:
"""Interpret Cohen's d effect size (Cohen, 1988)."""
if d < 0.2:
return "negligible"
elif d < 0.5:
return "small"
elif d < 0.8:
return "medium"
else:
return "large"
def generate_report(self, trajectories: List[List[Dict]], agent_name: str) -> str:
"""
Generate markdown report with all metrics.
Format suitable for inclusion in paper appendix.
"""
exploration = self.compute_exploration_rate(trajectories)
shape = self.compute_reward_trajectory_shape(trajectories)
actions = self.compute_action_distribution(trajectories)
calibration = self.compute_severity_calibration(trajectories)
# Compute aggregate scores
episode_scores = []
for ep in trajectories:
if ep:
rewards = [t.get("reward", {}).get("value", 0.0) for t in ep]
episode_scores.append(statistics.mean(rewards))
report = f"# Agent Profile: {agent_name}\n\n"
report += f"## Summary\n"
report += f"- Episodes: {len(trajectories)}\n"
if episode_scores:
report += f"- Mean score: {statistics.mean(episode_scores):.3f}\n"
if len(episode_scores) > 1:
report += f"- Score std: {statistics.stdev(episode_scores):.3f}\n"
report += f"- Exploration rate: {exploration:.3f}\n"
report += f"- Reward slope: {shape['slope']:.4f}\n"
report += f"- Monotonic episodes: {shape['monotonic_fraction']:.1%}\n\n"
report += f"## Action Distribution\n"
report += "| Action | Count | Fraction |\n"
report += "|--------|-------|----------|\n"
for atype, info in actions.items():
report += f"| {atype} | {info['count']} | {info['fraction']:.2%} |\n"
report += "\n"
report += f"## Severity Calibration\n"
report += "| Severity | Accuracy | Total | Correct |\n"
report += "|----------|----------|-------|---------|\n"
for sev, info in calibration.items():
report += f"| {sev} | {info['accuracy']:.2%} | {info['total']} | {info['correct']} |\n"
return report