Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions align_browser/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
build_manifest_from_experiments,
copy_experiment_files,
)
from align_browser.csv_exporter import write_experiments_to_csv


def copy_static_assets(output_dir):
Expand Down Expand Up @@ -74,6 +75,10 @@ def build_frontend(
with open(data_output_dir / "manifest.json", "w") as f:
json.dump(manifest.model_dump(), f, indent=2)

# Generate CSV export
csv_output_path = data_output_dir / "experiment_data.csv"
write_experiments_to_csv(experiments, experiments_root, csv_output_path)

return output_dir


Expand Down
213 changes: 213 additions & 0 deletions align_browser/csv_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""CSV export functionality for experiment data."""

import csv
from pathlib import Path
from typing import List, Dict, Any, Optional
from align_browser.experiment_models import (
ExperimentData,
InputOutputItem,
parse_alignment_target_id,
)


def format_kdma_config(kdma_values: List[Dict[str, Any]]) -> str:
"""Format KDMA values as a configuration string."""
if not kdma_values:
return "unaligned"

kdma_strings = []
for kdma in kdma_values:
kdma_name = kdma.get("kdma", "unknown")
value = kdma.get("value", 0.0)
kdma_strings.append(f"{kdma_name}:{value}")

return ",".join(kdma_strings)


def extract_choice_text(item: InputOutputItem) -> str:
"""Extract the human-readable choice text from an input/output item."""
if not item.output or "choice" not in item.output:
return ""

choice_index = item.output.get("choice")
if choice_index is None:
return ""

choices = item.input.choices
if not choices or choice_index >= len(choices):
return ""

selected_choice = choices[choice_index]
return selected_choice.get("unstructured", "")


def extract_choice_kdma(item: InputOutputItem) -> str:
"""Extract the KDMA association for the selected choice."""
if not item.output or "choice" not in item.output:
return ""

choice_index = item.output.get("choice")
if choice_index is None:
return ""

choices = item.input.choices
if not choices or choice_index >= len(choices):
return ""

selected_choice = choices[choice_index]
return selected_choice.get("kdma_association", "")


def extract_justification(item: InputOutputItem) -> str:
"""Extract the justification from an input/output item."""
if not item.output:
return ""

action = item.output.get("action", {})
return action.get("justification", "")


def get_decision_time(
timing_data: Optional[Dict[str, Any]], item_index: int
) -> Optional[float]:
"""Get the decision time for a specific item index from timing data."""
if not timing_data or "scenarios" not in timing_data:
return None

scenarios = timing_data.get("scenarios", [])
if item_index >= len(scenarios):
return None

scenario_timing = scenarios[item_index]
raw_times = scenario_timing.get("raw_times_s", [])

# Use the first raw time as the decision time for this scenario
if raw_times:
return raw_times[0]

# Fallback to average time if raw times not available
return scenario_timing.get("avg_time_s")


def get_score(
scores_data: Optional[Dict[str, Any]], item_index: int
) -> Optional[float]:
"""Get the score for a specific item index from scores data."""
if not scores_data or "scores" not in scores_data:
return None

scores_list = scores_data.get("scores", [])
if item_index >= len(scores_list):
return None

score_item = scores_list[item_index]
return score_item.get("score")


def experiment_to_csv_rows(
experiment: ExperimentData, experiments_root: Path
) -> List[Dict[str, Any]]:
"""Convert an experiment to CSV rows (one per scenario decision)."""
rows = []

# Get relative experiment path
relative_path = experiment.experiment_path.relative_to(experiments_root)
experiment_path_str = str(relative_path)

# Get experiment metadata
config = experiment.config
adm_name = config.adm.name if config.adm else "unknown"
llm_backbone = (
config.adm.llm_backbone if config.adm and config.adm.llm_backbone else "no_llm"
)
run_variant = config.run_variant if config.run_variant else "default"

# Parse alignment target to get KDMA config
alignment_target_id = (
config.alignment_target.id if config.alignment_target else "unaligned"
)
kdma_values = parse_alignment_target_id(alignment_target_id)
kdma_config = format_kdma_config(
[{"kdma": kv.kdma, "value": kv.value} for kv in kdma_values]
)

# Load timing data if available
timing_data = None
if experiment.timing:
timing_data = experiment.timing.model_dump()

# Load scores data if available
scores_data = None
if experiment.scores:
scores_data = experiment.scores.model_dump()

# Process each input/output item
for idx, item in enumerate(experiment.input_output.data):
row = {
"experiment_path": experiment_path_str,
"adm_name": adm_name,
"llm_backbone": llm_backbone,
"run_variant": run_variant,
"kdma_config": kdma_config,
"alignment_target_id": alignment_target_id,
"scenario_id": item.input.scenario_id,
"state_description": item.input.state
if hasattr(item.input, "state")
else "",
"choice_text": extract_choice_text(item),
"choice_kdma_association": extract_choice_kdma(item),
"justification": extract_justification(item),
"decision_time_s": get_decision_time(timing_data, idx),
"score": get_score(scores_data, idx),
}

# Convert None values to empty strings for CSV
for key, value in row.items():
if value is None:
row[key] = ""

rows.append(row)

return rows


def write_experiments_to_csv(
experiments: List[ExperimentData], experiments_root: Path, output_file: Path
) -> None:
"""Write all experiments to a CSV file."""

# Define CSV columns in order
fieldnames = [
"experiment_path",
"adm_name",
"llm_backbone",
"run_variant",
"kdma_config",
"alignment_target_id",
"scenario_id",
"state_description",
"choice_text",
"choice_kdma_association",
"justification",
"decision_time_s",
"score",
]

all_rows = []

# Convert all experiments to CSV rows
for experiment in experiments:
try:
rows = experiment_to_csv_rows(experiment, experiments_root)
all_rows.extend(rows)
except Exception as e:
print(
f"Warning: Failed to export experiment {experiment.experiment_path}: {e}"
)
continue

# Write CSV file
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(all_rows)
17 changes: 17 additions & 0 deletions align_browser/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ function preserveLinkedParameters(validatedParams, originalParams, appState) {
return preserved;
}

// CSV Download functionality
function downloadCSV() {
const csvPath = './data/experiment_data.csv';
const link = document.createElement('a');
link.href = csvPath;
link.download = 'experiment_data.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

document.addEventListener("DOMContentLoaded", () => {

// UI state persistence for expandable content
Expand Down Expand Up @@ -1084,6 +1095,12 @@ document.addEventListener("DOMContentLoaded", () => {
addColumnBtn.addEventListener('click', copyColumn);
}

// Add CSV download button event listener
const downloadCsvBtn = document.getElementById('download-csv-btn');
if (downloadCsvBtn) {
downloadCsvBtn.addEventListener('click', downloadCSV);
}

// Initial manifest fetch on page load
fetchManifest();
});
23 changes: 16 additions & 7 deletions align_browser/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@ <h2>
<span class="spinner"></span>
</span>
</h2>
<button
id="add-column-btn"
class="btn btn-primary"
style="display: none"
>
+ Add Column
</button>
<div class="header-buttons">
<button
id="download-csv-btn"
class="btn btn-secondary"
title="Download experiment data as CSV"
>
📥 Download CSV
</button>
<button
id="add-column-btn"
class="btn btn-primary"
style="display: none"
>
+ Add Column
</button>
</div>
</div>
<div id="runs-container" class="runs-container">
<div class="comparison-table-container">
Expand Down
15 changes: 15 additions & 0 deletions align_browser/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ footer {
padding-bottom: 15px;
}

.header-buttons {
display: flex;
gap: 10px;
align-items: center;
}

.table-header h2 {
margin: 0;
color: #495057;
Expand Down Expand Up @@ -341,6 +347,15 @@ footer {
background-color: #0056b3;
}

.btn-secondary {
background-color: #6c757d;
color: white;
}

.btn-secondary:hover:not(:disabled) {
background-color: #5a6268;
}


.btn:disabled {
background-color: #6c757d;
Expand Down
Loading