File size: 8,872 Bytes
a2ec7b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
"""
Simple Results Aggregator - Aggregate specific result directories
Usage: python -m src.aggregators.aggregate_specific_results --result-dir results/exp/model__service --k 4
"""

import json
import argparse
from pathlib import Path
from collections import defaultdict
from typing import Dict, Any, Tuple, List
from datetime import datetime
import sys
sys.path.append(str(Path(__file__).parent.parent.parent))
from src.aggregators.pricing import compute_cost_usd


def collect_results_from_dir(result_dir: Path, k: int) -> Dict[str, Any]:
    """Collect all results from a specific result directory."""
    results = {}

    for run_idx in range(1, k + 1):
        run_dir = result_dir / f"run-{run_idx}"
        if not run_dir.exists():
            print(f"โš ๏ธ  Warning: {run_dir} does not exist, skipping")
            continue

        run_results = {}
        for task_dir in run_dir.iterdir():
            if not task_dir.is_dir():
                continue

            meta_path = task_dir / "meta.json"
            if meta_path.exists():
                with open(meta_path) as f:
                    meta = json.load(f)
                    run_results[task_dir.name] = meta

        results[f"run-{run_idx}"] = run_results

    return results


def get_token_counts(meta: Dict[str, Any]) -> Tuple[int, int, int]:
    """Extract token counts from meta."""
    tu = meta.get("token_usage", {}) or {}
    input_tokens = int(tu.get("input_tokens", 0) or 0)
    output_tokens = int(tu.get("output_tokens", 0) or 0)
    total_tokens = int(tu.get("total_tokens", input_tokens + output_tokens) or (input_tokens + output_tokens))
    return input_tokens, output_tokens, total_tokens


def calculate_metrics(results: Dict, k: int, model_name: str) -> Dict:
    """Calculate metrics from results."""

    # Get all unique task names
    all_tasks = set()
    for run_name, run_data in results.items():
        all_tasks.update(run_data.keys())
    all_tasks = sorted(all_tasks)

    total_tasks = len(all_tasks)
    actual_runs = len(results)

    print(f"\n๐Ÿ“Š Analysis:")
    print(f"  Total unique tasks: {total_tasks}")
    print(f"  Runs found: {actual_runs} (expected: {k})")

    # Aggregates
    total_agent_execution_time = 0.0
    total_input_tokens = 0
    total_output_tokens = 0
    total_tokens = 0
    total_turns = 0

    actual_model_name = None

    # Per-run pass@1
    pass1_rates_per_run = []

    # For pass@k
    pass_k_task_success_any = 0
    pass_power_k_task_success_all = 0

    for run_idx in range(1, actual_runs + 1):
        run_name = f"run-{run_idx}"
        successes_this_run = 0

        for task in all_tasks:
            meta = results.get(run_name, {}).get(task)

            if not meta:
                continue

            success = bool(meta.get("execution_result", {}).get("success", False))
            if success:
                successes_this_run += 1

            total_agent_execution_time += float(meta.get("agent_execution_time", 0.0) or 0.0)
            in_tok, out_tok, ttl_tok = get_token_counts(meta)
            total_input_tokens += in_tok
            total_output_tokens += out_tok
            total_tokens += ttl_tok
            total_turns += int(meta.get("turn_count", 0) or 0)

            if actual_model_name is None:
                actual_model_name = meta.get("actual_model_name") or None

        pass1_rate = successes_this_run / total_tasks if total_tasks > 0 else 0
        pass1_rates_per_run.append(pass1_rate)
        print(f"  Run {run_idx}: {successes_this_run}/{total_tasks} = {pass1_rate*100:.1f}%")

    # Calculate pass@k
    for task in all_tasks:
        successes = []
        for run_idx in range(1, actual_runs + 1):
            run_name = f"run-{run_idx}"
            meta = results.get(run_name, {}).get(task)
            success = bool(meta.get("execution_result", {}).get("success", False)) if meta else False
            successes.append(success)

        if any(successes):
            pass_k_task_success_any += 1
        if all(successes):
            pass_power_k_task_success_all += 1

    # Averages
    denom = total_tasks * actual_runs if total_tasks > 0 else 1
    avg_agent_execution_time = total_agent_execution_time / denom
    avg_input_tokens = total_input_tokens / denom
    avg_output_tokens = total_output_tokens / denom
    avg_total_tokens = total_tokens / denom
    avg_turns = total_turns / denom

    # Pass@1 stats
    if pass1_rates_per_run:
        avg_pass1 = sum(pass1_rates_per_run) / len(pass1_rates_per_run)
        mean = avg_pass1
        variance = sum((r - mean) ** 2 for r in pass1_rates_per_run) / len(pass1_rates_per_run)
        std_pass1 = variance ** 0.5
    else:
        avg_pass1 = 0.0
        std_pass1 = 0.0

    # Cost calculation
    per_run_input_tokens = total_input_tokens / actual_runs if actual_runs else 0
    per_run_output_tokens = total_output_tokens / actual_runs if actual_runs else 0
    model_for_pricing = actual_model_name or model_name
    per_run_cost = compute_cost_usd(model_for_pricing, per_run_input_tokens, per_run_output_tokens)

    summary = {
        "generated_at": datetime.now().isoformat(),
        "model": model_name,
        "actual_model_name": actual_model_name or model_name,
        "runs": actual_runs,
        "total_tasks": total_tasks,
        "total_agent_execution_time": round(total_agent_execution_time, 2),
        "total_input_tokens": total_input_tokens,
        "total_output_tokens": total_output_tokens,
        "total_tokens": total_tokens,
        "total_turns": total_turns,
        "avg_agent_execution_time": round(avg_agent_execution_time, 4),
        "avg_input_tokens": round(avg_input_tokens, 2),
        "avg_output_tokens": round(avg_output_tokens, 2),
        "avg_total_tokens": round(avg_total_tokens, 2),
        "avg_turns": round(avg_turns, 2),
        "per_run_input_tokens": round(per_run_input_tokens, 2),
        "per_run_output_tokens": round(per_run_output_tokens, 2),
        "per_run_cost": round(per_run_cost, 4) if per_run_cost else None,
        "pass@1": {
            "avg": round(avg_pass1, 4),
            "std": round(std_pass1, 4),
            "per_run": [round(r, 4) for r in pass1_rates_per_run]
        },
    }

    if actual_runs > 1:
        summary[f"pass@{actual_runs}"] = round(pass_k_task_success_any / total_tasks, 4)
        summary[f"pass^{actual_runs}"] = round(pass_power_k_task_success_all / total_tasks, 4)

    return summary


def main():
    parser = argparse.ArgumentParser(description="Simple results aggregator for specific directories")
    parser.add_argument("--result-dir", required=True, help="Path to result directory (e.g., results/exp/model__service)")
    parser.add_argument("--k", type=int, default=4, help="Number of runs (default: 4)")
    parser.add_argument("--output", help="Output JSON file path (default: <result-dir>/summary.json)")

    args = parser.parse_args()

    result_dir = Path(args.result_dir)
    if not result_dir.exists():
        print(f"โŒ Result directory {result_dir} does not exist")
        return 1

    # Extract model name from directory name
    model_name = result_dir.name.replace("__", "-")

    print(f"๐Ÿ”„ Processing: {result_dir}")
    print(f"๐Ÿ“‹ Model: {model_name}")

    # Collect results
    results = collect_results_from_dir(result_dir, args.k)

    if not results:
        print("โŒ No results found")
        return 1

    # Calculate metrics
    summary = calculate_metrics(results, args.k, model_name)

    # Save summary
    output_path = Path(args.output) if args.output else result_dir / "summary.json"
    with open(output_path, "w") as f:
        json.dump(summary, f, indent=2)

    print(f"\nโœ… Summary saved to: {output_path}")
    print(f"\n๐Ÿ“ˆ Results:")
    print(f"  Pass@1: {summary['pass@1']['avg']*100:.1f}% ยฑ {summary['pass@1']['std']*100:.1f}%")
    if f"pass@{args.k}" in summary:
        print(f"  Pass@{args.k}: {summary[f'pass@{args.k}']*100:.1f}%")
        print(f"  Pass^{args.k}: {summary[f'pass^{args.k}']*100:.1f}%")
    print(f"  Per-run cost: ${summary['per_run_cost']:.4f}" if summary['per_run_cost'] else "  Per-run cost: N/A")
    print(f"  Avg agent time: {summary['avg_agent_execution_time']:.2f}s")
    print(f"  Avg turns: {summary['avg_turns']:.2f}")
    print(f"\n๐Ÿ“Š Token Usage:")
    avg_tokens_per_run = summary['total_tokens'] / summary['runs'] if summary['runs'] > 0 else 0
    print(f"  Avg tokens per run: {avg_tokens_per_run:,.0f}")
    print(f"  Avg tokens per turn: {summary['avg_total_tokens'] / summary['avg_turns']:.0f}" if summary['avg_turns'] > 0 else "  Avg tokens per turn: N/A")
    print(f"  Total tokens (all runs): {summary['total_tokens']:,}")
    print(f"  Total turns (all runs): {summary['total_turns']:,}")

    return 0


if __name__ == "__main__":
    exit(main())