-
Notifications
You must be signed in to change notification settings - Fork 976
Expand file tree
/
Copy pathvisualizer.py
More file actions
258 lines (210 loc) · 8.91 KB
/
visualizer.py
File metadata and controls
258 lines (210 loc) · 8.91 KB
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import os
import json
import glob
import logging
import shutil
import re as _re
from flask import Flask, render_template, render_template_string, jsonify
logger = logging.getLogger(__name__)
app = Flask(__name__, template_folder="templates")
def find_latest_checkpoint(base_folder):
# Check whether the base folder is itself a checkpoint folder
if os.path.basename(base_folder).startswith("checkpoint_"):
return base_folder
checkpoint_folders = glob.glob("**/checkpoint_*", root_dir=base_folder, recursive=True)
if not checkpoint_folders:
logger.info(f"No checkpoint folders found in {base_folder}")
return None
checkpoint_folders = [os.path.join(base_folder, folder) for folder in checkpoint_folders]
checkpoint_folders.sort(key=lambda x: os.path.getmtime(x), reverse=True)
logger.debug(f"Found checkpoint folder: {checkpoint_folders[0]}")
return checkpoint_folders[0]
def load_evolution_data(checkpoint_folder):
meta_path = os.path.join(checkpoint_folder, "metadata.json")
programs_dir = os.path.join(checkpoint_folder, "programs")
if not os.path.exists(meta_path) or not os.path.exists(programs_dir):
logger.info(f"Missing metadata.json or programs dir in {checkpoint_folder}")
return {"archive": [], "nodes": [], "edges": [], "checkpoint_dir": checkpoint_folder}
with open(meta_path) as f:
meta = json.load(f)
nodes = []
id_to_program = {}
pids = set()
# Build a map of program_id -> island_idx from islands data
pid_to_island = {}
for island_idx, id_list in enumerate(meta.get("islands", [])):
for pid in id_list:
pid_to_island[pid] = island_idx
# Start with programs from archive
archive = set(meta.get("archive", []))
to_load = list(archive)
loaded = set()
# Recursively load parent nodes even if they're not in archive
while to_load:
pid = to_load.pop(0)
if pid in loaded:
continue
loaded.add(pid)
prog_path = os.path.join(programs_dir, f"{pid}.json")
# Keep track of PIDs and if one is double, append "-copyN" to the PID
effective_pid = pid
if pid in pids:
base_pid = pid
if "-copy" in base_pid:
base_pid = base_pid.rsplit("-copy", 1)[0]
copy_num = 1
while f"{base_pid}-copy{copy_num}" in pids:
copy_num += 1
effective_pid = f"{base_pid}-copy{copy_num}"
pids.add(effective_pid)
if os.path.exists(prog_path):
with open(prog_path) as pf:
prog = json.load(pf)
prog["id"] = effective_pid
# Assign island index if the program is in an active island, otherwise -1
# Programs not in archive get island = -1 (historical/removed)
prog["island"] = pid_to_island.get(pid, -1) if pid in archive else -1
nodes.append(prog)
id_to_program[effective_pid] = prog
# Add parent to loading queue if it exists and hasn't been processed
parent_id = prog.get("parent_id")
if parent_id and parent_id not in loaded:
to_load.append(parent_id)
else:
logger.debug(f"Program file not found: {prog_path}")
edges = []
for prog in nodes:
parent_id = prog.get("parent_id")
if parent_id and parent_id in id_to_program:
edges.append({"source": parent_id, "target": prog["id"]})
logger.info(f"Loaded {len(nodes)} nodes and {len(edges)} edges from {checkpoint_folder}")
return {
"archive": meta.get("archive", []),
"nodes": nodes,
"edges": edges,
"checkpoint_dir": checkpoint_folder,
}
@app.route("/")
def index():
return render_template("index.html", checkpoint_dir=checkpoint_dir)
checkpoint_dir = None # Global variable to store the checkpoint directory
@app.route("/api/data")
def data():
global checkpoint_dir
base_folder = os.environ.get("EVOLVE_OUTPUT", "examples/")
checkpoint_dir = find_latest_checkpoint(base_folder)
if not checkpoint_dir:
logger.info(f"No checkpoints found in {base_folder}")
return jsonify({"archive": [], "nodes": [], "edges": [], "checkpoint_dir": ""})
logger.info(f"Loading data from checkpoint: {checkpoint_dir}")
data = load_evolution_data(checkpoint_dir)
logger.debug(f"Data: {data}")
return jsonify(data)
@app.route("/program/<program_id>")
def program_page(program_id):
global checkpoint_dir
if checkpoint_dir is None:
return "No checkpoint loaded", 500
data = load_evolution_data(checkpoint_dir)
program_data = next((p for p in data["nodes"] if p["id"] == program_id), None)
if program_data is None:
return "Program not found", 404
# Ensure program_data has required fields with safe defaults
program_data = {
"code": "",
"prompts": {},
"metrics": {},
"id": "",
"island": "",
"generation": 0,
"parent_id": None,
**program_data
}
# Ensure prompts is a dictionary
if not isinstance(program_data["prompts"], dict):
program_data["prompts"] = {}
artifacts_json = program_data.get("artifacts_json", None)
# Handle unicode escape for artifacts JSON display - same as in sidebar.js
if artifacts_json and isinstance(artifacts_json, str):
try:
# Parse and stringify to properly escape unicode
parsed = json.loads(artifacts_json)
artifacts_json = json.dumps(parsed, indent=2, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
# If parsing fails, use original value
pass
return render_template(
"program_page.html",
program_data=program_data,
checkpoint_dir=checkpoint_dir,
artifacts_json=artifacts_json,
)
def run_static_export(args):
output_dir = args.static_output
os.makedirs(output_dir, exist_ok=True)
# Load data and prepare JSON string
checkpoint_dir = find_latest_checkpoint(args.path)
if not checkpoint_dir:
raise RuntimeError(f"No checkpoint found in {args.path}")
data = load_evolution_data(checkpoint_dir)
logger.info(f"Exporting visualization for checkpoint: {checkpoint_dir}")
with app.app_context():
data_json = jsonify(data).get_data(as_text=True)
inlined = f"<script>window.STATIC_DATA = {data_json};</script>"
# Load index.html template
templates_dir = os.path.join(os.path.dirname(__file__), "templates")
template_path = os.path.join(templates_dir, "index.html")
with open(template_path, "r", encoding="utf-8") as f:
html = f.read()
# Insert static json data into the HTML
html = _re.sub(r"\{\{\s*url_for\('static', filename='([^']+)'\)\s*\}\}", r"static/\1", html)
script_tag_idx = html.find('<script type="module"')
if script_tag_idx != -1:
html = html[:script_tag_idx] + inlined + "\n" + html[script_tag_idx:]
else:
html = html.replace("</body>", inlined + "\n</body>")
with open(os.path.join(output_dir, "index.html"), "w", encoding="utf-8") as f:
f.write(html)
# Copy over static files
static_src = os.path.join(os.path.dirname(__file__), "static")
static_dst = os.path.join(output_dir, "static")
if os.path.exists(static_dst):
shutil.rmtree(static_dst)
shutil.copytree(static_src, static_dst)
logger.info(
f"Static export written to {output_dir}/\nNote: This will only work correctly with a web server, not by opening the HTML file directly in a browser. Try $ python3 -m http.server --directory {output_dir} 8080"
)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="OpenEvolve Evolution Visualizer")
parser.add_argument(
"--path",
type=str,
default="examples/",
help="Path to openevolve_output or checkpoints folder",
)
parser.add_argument("--host", type=str, default="127.0.0.1")
parser.add_argument("--port", type=int, default=8080)
parser.add_argument(
"--log-level",
type=str,
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
parser.add_argument(
"--static-output",
type=str,
default=None,
help="Produce a static HTML export in this directory and exit.",
)
args = parser.parse_args()
log_level = getattr(logging, args.log_level.upper(), logging.INFO)
logging.basicConfig(level=log_level, format="[%(asctime)s] %(levelname)s %(name)s: %(message)s")
logger.info(f"Current working directory: {os.getcwd()}")
if args.static_output:
run_static_export(args)
os.environ["EVOLVE_OUTPUT"] = args.path
logger.info(
f"Starting server at http://{args.host}:{args.port} with log level {args.log_level.upper()}"
)
app.run(host=args.host, port=args.port, debug=True)