weco 0.1.10__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weco/__init__.py +4 -4
- weco/api.py +89 -0
- weco/cli.py +333 -0
- weco/panels.py +359 -0
- weco/utils.py +114 -175
- weco-0.2.0.dist-info/METADATA +129 -0
- weco-0.2.0.dist-info/RECORD +11 -0
- {weco-0.1.10.dist-info → weco-0.2.0.dist-info}/WHEEL +1 -1
- weco-0.2.0.dist-info/entry_points.txt +2 -0
- {weco-0.1.10.dist-info → weco-0.2.0.dist-info/licenses}/LICENSE +2 -1
- weco/client.py +0 -586
- weco/constants.py +0 -4
- weco/functional.py +0 -184
- weco-0.1.10.dist-info/METADATA +0 -125
- weco-0.1.10.dist-info/RECORD +0 -10
- {weco-0.1.10.dist-info → weco-0.2.0.dist-info}/top_level.txt +0 -0
weco/panels.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from rich.tree import Tree
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich.progress import BarColumn, Progress, TextColumn
|
|
4
|
+
from rich.layout import Layout
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.syntax import Syntax
|
|
7
|
+
from typing import Dict, List, Optional, Union, Tuple
|
|
8
|
+
from .utils import format_number, truncate_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SummaryPanel:
|
|
12
|
+
"""Holds a summary of the optimization session."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, total_steps: int, model: str, session_id: str = None):
|
|
15
|
+
self.total_input_tokens = 0
|
|
16
|
+
self.total_output_tokens = 0
|
|
17
|
+
self.total_steps = total_steps
|
|
18
|
+
self.model = model
|
|
19
|
+
self.session_id = session_id or "unknown"
|
|
20
|
+
self.progress = Progress(
|
|
21
|
+
TextColumn("[progress.description]{task.description}"),
|
|
22
|
+
BarColumn(bar_width=20),
|
|
23
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
24
|
+
TextColumn("•"),
|
|
25
|
+
TextColumn("[bold]{task.completed}/{task.total} Steps"),
|
|
26
|
+
expand=False,
|
|
27
|
+
)
|
|
28
|
+
self.task_id = self.progress.add_task("", total=total_steps)
|
|
29
|
+
|
|
30
|
+
def set_step(self, step: int):
|
|
31
|
+
"""Set the current step."""
|
|
32
|
+
self.progress.update(self.task_id, completed=step)
|
|
33
|
+
|
|
34
|
+
def update_token_counts(self, usage: Dict[str, int]):
|
|
35
|
+
"""Update token counts from usage data."""
|
|
36
|
+
if not isinstance(usage, dict) or "input_tokens" not in usage or "output_tokens" not in usage:
|
|
37
|
+
raise ValueError("Invalid token usage response from API.")
|
|
38
|
+
self.total_input_tokens += usage["input_tokens"]
|
|
39
|
+
self.total_output_tokens += usage["output_tokens"]
|
|
40
|
+
|
|
41
|
+
def get_display(self) -> Panel:
|
|
42
|
+
"""Create a summary panel with the relevant information."""
|
|
43
|
+
layout = Layout(name="summary")
|
|
44
|
+
summary_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
45
|
+
# Log directory
|
|
46
|
+
runs_dir = f".runs/{self.session_id}"
|
|
47
|
+
summary_table.add_row(f"[bold cyan]Logs:[/] [blue]{runs_dir}[/]")
|
|
48
|
+
summary_table.add_row("")
|
|
49
|
+
# Model used
|
|
50
|
+
summary_table.add_row(f"[bold cyan]Model:[/] {self.model}")
|
|
51
|
+
summary_table.add_row("")
|
|
52
|
+
# Token counts
|
|
53
|
+
summary_table.add_row(
|
|
54
|
+
f"[bold cyan]Tokens:[/] ↑[yellow]{format_number(self.total_input_tokens)}[/] ↓[yellow]{format_number(self.total_output_tokens)}[/] = [green]{format_number(self.total_input_tokens + self.total_output_tokens)}[/]"
|
|
55
|
+
)
|
|
56
|
+
# Progress bar
|
|
57
|
+
summary_table.add_row(self.progress)
|
|
58
|
+
|
|
59
|
+
# Update layout
|
|
60
|
+
layout.update(summary_table)
|
|
61
|
+
|
|
62
|
+
return Panel(layout, title="[bold]📊 Summary", border_style="magenta", expand=True, padding=(0, 1))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PlanPanel:
|
|
66
|
+
"""Displays the optimization plan with truncation for long plans."""
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
self.plan = ""
|
|
70
|
+
self.max_lines = 3 # Approximate number of lines that can fit in the panel
|
|
71
|
+
|
|
72
|
+
def update(self, plan: str):
|
|
73
|
+
"""Update the plan text."""
|
|
74
|
+
self.plan = plan
|
|
75
|
+
|
|
76
|
+
def clear(self):
|
|
77
|
+
"""Clear the plan text."""
|
|
78
|
+
self.plan = ""
|
|
79
|
+
|
|
80
|
+
def get_display(self) -> Panel:
|
|
81
|
+
"""Create a panel displaying the plan with truncation if needed."""
|
|
82
|
+
display_text = truncate_text(text=self.plan, max_lines=self.max_lines)
|
|
83
|
+
return Panel(
|
|
84
|
+
display_text,
|
|
85
|
+
title="[bold]📝 Thinking...",
|
|
86
|
+
border_style="cyan",
|
|
87
|
+
expand=True,
|
|
88
|
+
padding=(0, 1),
|
|
89
|
+
subtitle="[cyan]↓ truncated ↓[/]" if len(self.plan) > self.max_lines else None,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Node:
|
|
94
|
+
"""Represents a node in the solution tree."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self, id: str, parent_id: Union[str, None], code: Union[str, None], metric: Union[float, None], is_buggy: bool
|
|
98
|
+
):
|
|
99
|
+
self.id = id
|
|
100
|
+
self.parent_id = parent_id
|
|
101
|
+
self.children: List["Node"] = []
|
|
102
|
+
self.code = code
|
|
103
|
+
self.metric = metric
|
|
104
|
+
self.is_buggy = is_buggy
|
|
105
|
+
self.evaluated = True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MetricTree:
|
|
109
|
+
"""Manages the tree structure of optimization solutions."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, maximize: bool):
|
|
112
|
+
self.nodes: Dict[str, Node] = {}
|
|
113
|
+
self.maximize = maximize
|
|
114
|
+
|
|
115
|
+
def clear(self):
|
|
116
|
+
"""Clear the tree."""
|
|
117
|
+
self.nodes = {}
|
|
118
|
+
|
|
119
|
+
def add_node(self, node: Node):
|
|
120
|
+
"""Add a node to the tree."""
|
|
121
|
+
# Add the node to the tree
|
|
122
|
+
self.nodes[node.id] = node
|
|
123
|
+
|
|
124
|
+
# Add node to node's parent's children
|
|
125
|
+
if node.parent_id is not None:
|
|
126
|
+
if node.parent_id not in self.nodes:
|
|
127
|
+
raise ValueError("Could not construct tree: parent node not found.")
|
|
128
|
+
self.nodes[node.parent_id].children.append(node)
|
|
129
|
+
|
|
130
|
+
def get_draft_nodes(self) -> List[Node]:
|
|
131
|
+
"""Get all draft nodes from the tree."""
|
|
132
|
+
return [node for node in self.nodes.values() if node.parent_id is None]
|
|
133
|
+
|
|
134
|
+
def get_best_node(self) -> Optional[Node]:
|
|
135
|
+
"""Get the best node from the tree."""
|
|
136
|
+
measured_nodes = [
|
|
137
|
+
node
|
|
138
|
+
for node in self.nodes.values()
|
|
139
|
+
if node.evaluated # evaluated
|
|
140
|
+
and not node.is_buggy # not buggy
|
|
141
|
+
and node.metric is not None # has metric
|
|
142
|
+
]
|
|
143
|
+
if len(measured_nodes) == 0:
|
|
144
|
+
return None
|
|
145
|
+
if self.maximize:
|
|
146
|
+
return max(measured_nodes, key=lambda node: node.metric)
|
|
147
|
+
else:
|
|
148
|
+
return min(measured_nodes, key=lambda node: node.metric)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class MetricTreePanel:
|
|
152
|
+
"""Displays the solution tree with depth limiting."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, maximize: bool):
|
|
155
|
+
self.metric_tree = MetricTree(maximize=maximize)
|
|
156
|
+
|
|
157
|
+
def build_metric_tree(self, nodes: List[dict]):
|
|
158
|
+
"""Build the tree from the list of nodes."""
|
|
159
|
+
# First clear then tree
|
|
160
|
+
self.metric_tree.clear()
|
|
161
|
+
|
|
162
|
+
# Then sort the nodes by step number
|
|
163
|
+
nodes.sort(key=lambda x: x["step"])
|
|
164
|
+
|
|
165
|
+
# Finally build the new tree
|
|
166
|
+
for node in nodes:
|
|
167
|
+
self.metric_tree.add_node(
|
|
168
|
+
Node(
|
|
169
|
+
id=node["solution_id"],
|
|
170
|
+
parent_id=node["parent_id"],
|
|
171
|
+
code=node["code"],
|
|
172
|
+
metric=node["metric_value"],
|
|
173
|
+
is_buggy=node["is_buggy"],
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def set_unevaluated_node(self, node_id: str):
|
|
178
|
+
"""Set the unevaluated node."""
|
|
179
|
+
self.metric_tree.nodes[node_id].evaluated = False
|
|
180
|
+
|
|
181
|
+
def _build_rich_tree(self) -> Tree:
|
|
182
|
+
"""Get a Rich Tree representation of the solution tree using a DFS like traversal."""
|
|
183
|
+
if len(self.metric_tree.nodes) == 0:
|
|
184
|
+
return Tree("[bold green]Building 🌳")
|
|
185
|
+
|
|
186
|
+
best_node = self.metric_tree.get_best_node()
|
|
187
|
+
|
|
188
|
+
def append_rec(node: Node, tree: Tree):
|
|
189
|
+
if not node.evaluated:
|
|
190
|
+
# not evaluated
|
|
191
|
+
color = "yellow"
|
|
192
|
+
style = None
|
|
193
|
+
text = "evaluating..."
|
|
194
|
+
elif node.is_buggy:
|
|
195
|
+
# buggy node
|
|
196
|
+
color = "red"
|
|
197
|
+
style = None
|
|
198
|
+
text = "bug"
|
|
199
|
+
else:
|
|
200
|
+
# evaluated non-buggy node
|
|
201
|
+
if node.id == best_node.id:
|
|
202
|
+
# best node
|
|
203
|
+
color = "green"
|
|
204
|
+
style = "bold"
|
|
205
|
+
text = f"{node.metric:.3f} (best)"
|
|
206
|
+
elif node.metric is None:
|
|
207
|
+
# metric not extracted from evaluated solution
|
|
208
|
+
color = "yellow"
|
|
209
|
+
style = None
|
|
210
|
+
text = "N/A"
|
|
211
|
+
else:
|
|
212
|
+
# evaluated node with metric
|
|
213
|
+
color = "green"
|
|
214
|
+
style = None
|
|
215
|
+
text = f"{node.metric:.3f}"
|
|
216
|
+
|
|
217
|
+
s = f"[{f'{style} ' if style is not None else ''}{color}]● {text}"
|
|
218
|
+
subtree = tree.add(s)
|
|
219
|
+
for child in node.children:
|
|
220
|
+
append_rec(child, subtree)
|
|
221
|
+
|
|
222
|
+
tree = Tree("🌳")
|
|
223
|
+
for n in self.metric_tree.get_draft_nodes():
|
|
224
|
+
append_rec(n, tree)
|
|
225
|
+
|
|
226
|
+
return tree
|
|
227
|
+
|
|
228
|
+
def get_display(self) -> Panel:
|
|
229
|
+
"""Get a panel displaying the solution tree."""
|
|
230
|
+
# Make sure the metric tree is built before calling build_rich_tree
|
|
231
|
+
return Panel(
|
|
232
|
+
self._build_rich_tree(), title="[bold]🌳 Solution Tree", border_style="green", expand=True, padding=(0, 1)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class EvaluationOutputPanel:
|
|
237
|
+
"""Displays evaluation output with truncation for long outputs."""
|
|
238
|
+
|
|
239
|
+
def __init__(self):
|
|
240
|
+
self.output = ""
|
|
241
|
+
self.max_lines = 25 # Approximate number of lines that can fit in the panel
|
|
242
|
+
|
|
243
|
+
def update(self, output: str) -> None:
|
|
244
|
+
"""Update the evaluation output."""
|
|
245
|
+
self.output = output
|
|
246
|
+
|
|
247
|
+
def clear(self) -> None:
|
|
248
|
+
"""Clear the evaluation output."""
|
|
249
|
+
self.output = ""
|
|
250
|
+
|
|
251
|
+
def get_display(self) -> Panel:
|
|
252
|
+
"""Create a panel displaying the evaluation output with truncation if needed."""
|
|
253
|
+
display_text = truncate_text(text=self.output, max_lines=self.max_lines)
|
|
254
|
+
|
|
255
|
+
title = "[bold]📋 Evaluation Output"
|
|
256
|
+
if len(self.output) == len(display_text):
|
|
257
|
+
title += " (truncated)[/]"
|
|
258
|
+
|
|
259
|
+
return Panel(display_text, title=title, border_style="red", expand=True, padding=(0, 1))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class SolutionPanels:
|
|
263
|
+
"""Displays the current and best solutions side by side."""
|
|
264
|
+
|
|
265
|
+
def __init__(self):
|
|
266
|
+
# Current solution
|
|
267
|
+
self.current_node = None
|
|
268
|
+
# Best solution
|
|
269
|
+
self.best_node = None
|
|
270
|
+
self.max_lines = 30 # Approximate number of lines that can fit in each panel
|
|
271
|
+
|
|
272
|
+
def update(self, current_node: Union[Node, None], best_node: Union[Node, None]):
|
|
273
|
+
"""Update the current and best solutions."""
|
|
274
|
+
# Update current solution
|
|
275
|
+
self.current_node = current_node
|
|
276
|
+
# Update best solution
|
|
277
|
+
self.best_node = best_node
|
|
278
|
+
|
|
279
|
+
def get_display(self, current_step: int) -> Tuple[Panel, Panel]:
|
|
280
|
+
"""Return the current and best solutions as panels."""
|
|
281
|
+
current_code = self.current_node.code if self.current_node is not None else ""
|
|
282
|
+
best_code = self.best_node.code if self.best_node is not None else ""
|
|
283
|
+
best_score = self.best_node.metric if self.best_node is not None else None
|
|
284
|
+
|
|
285
|
+
# Determine if code is too long (approximate)
|
|
286
|
+
current_lines = current_code.count("\n") + 1
|
|
287
|
+
best_lines = best_code.count("\n") + 1
|
|
288
|
+
|
|
289
|
+
# Current solution (without score)
|
|
290
|
+
current_title = f"[bold]💡 Current Solution (Step {current_step})"
|
|
291
|
+
current_panel = Panel(
|
|
292
|
+
Syntax(
|
|
293
|
+
str(current_code),
|
|
294
|
+
"python",
|
|
295
|
+
theme="monokai",
|
|
296
|
+
line_numbers=True,
|
|
297
|
+
word_wrap=False,
|
|
298
|
+
line_range=(0, self.max_lines), # Only show first max_lines lines
|
|
299
|
+
),
|
|
300
|
+
title=current_title,
|
|
301
|
+
border_style="yellow",
|
|
302
|
+
expand=True,
|
|
303
|
+
padding=(0, 1),
|
|
304
|
+
subtitle="[yellow]↓ truncated ↓[/]" if current_lines > self.max_lines else None,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Best solution
|
|
308
|
+
best_title = f"[bold]🏆 Best Solution ([green]Score: {f'{best_score:.4f}' if best_score is not None else 'N/A'}[/])"
|
|
309
|
+
best_panel = Panel(
|
|
310
|
+
Syntax(
|
|
311
|
+
str(best_code),
|
|
312
|
+
"python",
|
|
313
|
+
theme="monokai",
|
|
314
|
+
line_numbers=True,
|
|
315
|
+
word_wrap=False,
|
|
316
|
+
line_range=(0, self.max_lines), # Only show first max_lines lines
|
|
317
|
+
),
|
|
318
|
+
title=best_title,
|
|
319
|
+
border_style="green",
|
|
320
|
+
expand=True,
|
|
321
|
+
padding=(0, 1),
|
|
322
|
+
subtitle="[yellow]↓ truncated ↓[/]" if best_lines > self.max_lines else None,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return current_panel, best_panel
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def create_optimization_layout() -> Layout:
|
|
329
|
+
"""Create the main layout for the CLI."""
|
|
330
|
+
layout = Layout()
|
|
331
|
+
|
|
332
|
+
# First split into top, middle, and bottom sections
|
|
333
|
+
layout.split_column(
|
|
334
|
+
Layout(name="top_section", ratio=3), Layout(name="middle_section", ratio=4), Layout(name="eval_output", ratio=2)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Split the top section into left and right
|
|
338
|
+
layout["top_section"].split_row(Layout(name="left_panels", ratio=1), Layout(name="tree", ratio=1))
|
|
339
|
+
|
|
340
|
+
# Split the left panels into summary and thinking
|
|
341
|
+
layout["left_panels"].split_column(Layout(name="summary", ratio=2), Layout(name="plan", ratio=1))
|
|
342
|
+
|
|
343
|
+
# Split the middle section into left and right
|
|
344
|
+
layout["middle_section"].split_row(Layout(name="current_solution", ratio=1), Layout(name="best_solution", ratio=1))
|
|
345
|
+
|
|
346
|
+
return layout
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def create_end_optimization_layout() -> Layout:
|
|
350
|
+
"""Create the final layout after optimization is complete."""
|
|
351
|
+
layout = Layout()
|
|
352
|
+
|
|
353
|
+
# Create a top section for summary
|
|
354
|
+
layout.split_column(Layout(name="summary", ratio=1), Layout(name="bottom_section", ratio=3))
|
|
355
|
+
|
|
356
|
+
# Split the bottom section into left (best solution) and right ( tree)
|
|
357
|
+
layout["bottom_section"].split_row(Layout(name="best_solution", ratio=1), Layout(name="tree", ratio=1))
|
|
358
|
+
|
|
359
|
+
return layout
|
weco/utils.py
CHANGED
|
@@ -1,180 +1,119 @@
|
|
|
1
|
-
import
|
|
1
|
+
from typing import Any, Dict, List, Tuple, Union
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from
|
|
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
|
-
except IOError:
|
|
34
|
-
return False
|
|
35
|
-
|
|
36
|
-
return True
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def is_base64_image(maybe_base64: str) -> Tuple[bool, Optional[Dict[str, str]]]:
|
|
40
|
-
"""
|
|
41
|
-
Check if the image is a base64 encoded image and if so, extract the image information from the encoded data or URL provided.
|
|
42
|
-
|
|
43
|
-
Parameters
|
|
44
|
-
----------
|
|
45
|
-
data : str
|
|
46
|
-
The image data or URL.
|
|
47
|
-
|
|
48
|
-
Returns
|
|
49
|
-
-------
|
|
50
|
-
Tuple[bool, Optional[Dict[str, str]]]
|
|
51
|
-
"""
|
|
52
|
-
pattern = r"data:(?P<media_type>[\w/]+);(?P<source_type>\w+),(?P<encoding>.*)"
|
|
53
|
-
match = re.match(pattern, maybe_base64)
|
|
54
|
-
if match:
|
|
55
|
-
return True, match.groupdict()
|
|
56
|
-
|
|
57
|
-
return False, None
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def is_public_url_image(maybe_url_image: str) -> bool:
|
|
61
|
-
"""
|
|
62
|
-
Check if the string is a publicly accessible URL
|
|
63
|
-
|
|
64
|
-
Parameters
|
|
65
|
-
----------
|
|
66
|
-
maybe_url_image : str
|
|
67
|
-
The URL to check.
|
|
68
|
-
|
|
69
|
-
Returns
|
|
70
|
-
-------
|
|
71
|
-
bool
|
|
72
|
-
True if the URL is publicly accessible, False otherwise.
|
|
73
|
-
"""
|
|
74
|
-
try:
|
|
75
|
-
# Check if it is a valid URL
|
|
76
|
-
if not urlparse(maybe_url_image).scheme:
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
# Check if the URL is publicly accessible
|
|
80
|
-
response = requests.head(maybe_url_image)
|
|
81
|
-
if response.status_code != 200:
|
|
82
|
-
return False
|
|
83
|
-
|
|
84
|
-
# Check if the URL is an image
|
|
85
|
-
content_type = response.headers.get("content-type")
|
|
86
|
-
if not content_type:
|
|
87
|
-
return False
|
|
88
|
-
if not content_type.startswith("image"):
|
|
89
|
-
return False
|
|
90
|
-
except Exception:
|
|
91
|
-
return False
|
|
92
|
-
|
|
93
|
-
return True
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def get_image_size(image: str, source: str) -> float:
|
|
97
|
-
"""
|
|
98
|
-
Get the size of the image in MB.
|
|
99
|
-
|
|
100
|
-
Parameters
|
|
101
|
-
----------
|
|
102
|
-
image : str
|
|
103
|
-
The image data or URL.
|
|
104
|
-
|
|
105
|
-
source : str
|
|
106
|
-
The source of the image. It can be 'base64', 'url', or 'local'.
|
|
107
|
-
|
|
108
|
-
Returns
|
|
109
|
-
-------
|
|
110
|
-
float
|
|
111
|
-
The size of the image in MB.
|
|
112
|
-
|
|
113
|
-
Raises
|
|
114
|
-
------
|
|
115
|
-
ValueError
|
|
116
|
-
If the image is not a valid image.
|
|
117
|
-
"""
|
|
118
|
-
if source == "base64":
|
|
119
|
-
_, base64_info = is_base64_image(maybe_base64=image)
|
|
120
|
-
img_data = base64.b64decode(base64_info["encoding"])
|
|
121
|
-
elif source == "url":
|
|
122
|
-
response = requests.get(image)
|
|
123
|
-
response.raise_for_status()
|
|
124
|
-
img_data = response.content
|
|
125
|
-
elif source == "local":
|
|
126
|
-
with open(image, "rb") as f:
|
|
127
|
-
img_data = f.read()
|
|
4
|
+
import time
|
|
5
|
+
import subprocess
|
|
6
|
+
from rich.layout import Layout
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
import pathlib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Env/arg helper functions
|
|
13
|
+
def read_api_keys_from_env() -> Dict[str, Any]:
|
|
14
|
+
"""Read API keys from environment variables."""
|
|
15
|
+
keys = {}
|
|
16
|
+
openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
17
|
+
anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
18
|
+
if openai_api_key is not None and len(openai_api_key) > 0:
|
|
19
|
+
keys["OPENAI_API_KEY"] = openai_api_key
|
|
20
|
+
if anthropic_api_key is not None and len(anthropic_api_key) > 0:
|
|
21
|
+
keys["ANTHROPIC_API_KEY"] = anthropic_api_key
|
|
22
|
+
return keys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_additional_instructions(additional_instructions: str | None) -> str | None:
|
|
26
|
+
"""Read additional instructions from a file path string or return the string itself."""
|
|
27
|
+
if additional_instructions is None:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
# Try interpreting as a path first
|
|
31
|
+
potential_path = pathlib.Path(additional_instructions)
|
|
32
|
+
if potential_path.exists() and potential_path.is_file():
|
|
33
|
+
return read_from_path(potential_path, is_json=False) # type: ignore # read_from_path returns str when is_json=False
|
|
128
34
|
else:
|
|
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
|
-
|
|
35
|
+
# If it's not a valid file path, return the string itself
|
|
36
|
+
return additional_instructions
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# File helper functions
|
|
40
|
+
def read_from_path(fp: pathlib.Path, is_json: bool = False) -> Union[str, Dict[str, Any]]:
|
|
41
|
+
"""Read content from a file path, optionally parsing as JSON."""
|
|
42
|
+
with fp.open("r", encoding="utf-8") as f:
|
|
43
|
+
if is_json:
|
|
44
|
+
return json.load(f)
|
|
45
|
+
return f.read()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_to_path(fp: pathlib.Path, content: Union[str, Dict[str, Any]], is_json: bool = False) -> None:
|
|
49
|
+
"""Write content to a file path, optionally as JSON."""
|
|
50
|
+
with fp.open("w", encoding="utf-8") as f:
|
|
51
|
+
if is_json:
|
|
52
|
+
json.dump(content, f, indent=4)
|
|
53
|
+
elif isinstance(content, str):
|
|
54
|
+
f.write(content)
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError("Content must be str or Dict[str, Any]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Visualization helper functions
|
|
60
|
+
def format_number(n: Union[int, float]) -> str:
|
|
61
|
+
"""Format large numbers with K, M, B, T suffixes for better readability."""
|
|
62
|
+
if n >= 1e12:
|
|
63
|
+
return f"{n / 1e12:.1f}T"
|
|
64
|
+
elif n >= 1e9:
|
|
65
|
+
return f"{n / 1e9:.1f}B"
|
|
66
|
+
elif n >= 1e6:
|
|
67
|
+
return f"{n / 1e6:.1f}M"
|
|
68
|
+
elif n >= 1e3:
|
|
69
|
+
return f"{n / 1e3:.1f}K"
|
|
70
|
+
# Handle potential floats that don't need suffix but might need formatting
|
|
71
|
+
if isinstance(n, float):
|
|
72
|
+
# Format floats nicely, avoid excessive precision unless needed
|
|
73
|
+
return f"{n:.4g}" # Use general format, up to 4 significant digits
|
|
74
|
+
return str(n)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def smooth_update(
|
|
78
|
+
live: Live, layout: Layout, sections_to_update: List[Tuple[str, Panel]], transition_delay: float = 0.05
|
|
79
|
+
) -> None:
|
|
167
80
|
"""
|
|
168
|
-
|
|
81
|
+
Update sections of the layout with a small delay between each update for a smoother transition effect.
|
|
169
82
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Returns
|
|
176
|
-
-------
|
|
177
|
-
str
|
|
178
|
-
The random base16 code.
|
|
83
|
+
Args:
|
|
84
|
+
live: The Live display instance
|
|
85
|
+
layout: The Layout to update
|
|
86
|
+
sections_to_update: List of (section_name, content) tuples to update
|
|
87
|
+
transition_delay: Delay in seconds between updates (default: 0.05)
|
|
179
88
|
"""
|
|
180
|
-
|
|
89
|
+
for section, content in sections_to_update:
|
|
90
|
+
layout[section].update(content)
|
|
91
|
+
live.refresh()
|
|
92
|
+
time.sleep(transition_delay)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Other helper functions
|
|
96
|
+
def run_evaluation(eval_command: str) -> str:
|
|
97
|
+
"""Run the evaluation command on the code and return the output."""
|
|
98
|
+
|
|
99
|
+
# Run the eval command as is
|
|
100
|
+
result = subprocess.run(eval_command, shell=True, capture_output=True, text=True, check=False)
|
|
101
|
+
|
|
102
|
+
# Combine stdout and stderr for complete output
|
|
103
|
+
output = result.stderr if result.stderr else ""
|
|
104
|
+
if result.stdout:
|
|
105
|
+
if len(output) > 0:
|
|
106
|
+
output += "\n"
|
|
107
|
+
output += result.stdout
|
|
108
|
+
return output
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def truncate_text(text: str, max_lines: int) -> str:
|
|
112
|
+
"""Truncate text to a maximum number of lines."""
|
|
113
|
+
lines = text.split("\n")
|
|
114
|
+
if len(lines) > max_lines:
|
|
115
|
+
display_lines = lines[:max_lines]
|
|
116
|
+
display_text = "\n".join(display_lines)
|
|
117
|
+
else:
|
|
118
|
+
display_text = text
|
|
119
|
+
return display_text
|