zero-agent 0.1.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.
- agentz/agent/base.py +262 -0
- agentz/artifacts/__init__.py +5 -0
- agentz/artifacts/artifact_writer.py +538 -0
- agentz/artifacts/reporter.py +235 -0
- agentz/artifacts/terminal_writer.py +100 -0
- agentz/context/__init__.py +6 -0
- agentz/context/context.py +91 -0
- agentz/context/conversation.py +205 -0
- agentz/context/data_store.py +208 -0
- agentz/llm/llm_setup.py +156 -0
- agentz/mcp/manager.py +142 -0
- agentz/mcp/patches.py +88 -0
- agentz/mcp/servers/chrome_devtools/server.py +14 -0
- agentz/profiles/base.py +108 -0
- agentz/profiles/data/data_analysis.py +38 -0
- agentz/profiles/data/data_loader.py +35 -0
- agentz/profiles/data/evaluation.py +43 -0
- agentz/profiles/data/model_training.py +47 -0
- agentz/profiles/data/preprocessing.py +47 -0
- agentz/profiles/data/visualization.py +47 -0
- agentz/profiles/manager/evaluate.py +51 -0
- agentz/profiles/manager/memory.py +62 -0
- agentz/profiles/manager/observe.py +48 -0
- agentz/profiles/manager/routing.py +66 -0
- agentz/profiles/manager/writer.py +51 -0
- agentz/profiles/mcp/browser.py +21 -0
- agentz/profiles/mcp/chrome.py +21 -0
- agentz/profiles/mcp/notion.py +21 -0
- agentz/runner/__init__.py +74 -0
- agentz/runner/base.py +28 -0
- agentz/runner/executor.py +320 -0
- agentz/runner/hooks.py +110 -0
- agentz/runner/iteration.py +142 -0
- agentz/runner/patterns.py +215 -0
- agentz/runner/tracker.py +188 -0
- agentz/runner/utils.py +45 -0
- agentz/runner/workflow.py +250 -0
- agentz/tools/__init__.py +20 -0
- agentz/tools/data_tools/__init__.py +17 -0
- agentz/tools/data_tools/data_analysis.py +152 -0
- agentz/tools/data_tools/data_loading.py +92 -0
- agentz/tools/data_tools/evaluation.py +175 -0
- agentz/tools/data_tools/helpers.py +120 -0
- agentz/tools/data_tools/model_training.py +192 -0
- agentz/tools/data_tools/preprocessing.py +229 -0
- agentz/tools/data_tools/visualization.py +281 -0
- agentz/utils/__init__.py +69 -0
- agentz/utils/config.py +708 -0
- agentz/utils/helpers.py +10 -0
- agentz/utils/parsers.py +142 -0
- agentz/utils/printer.py +539 -0
- pipelines/base.py +972 -0
- pipelines/data_scientist.py +97 -0
- pipelines/data_scientist_memory.py +151 -0
- pipelines/experience_learner.py +0 -0
- pipelines/prompt_generator.py +0 -0
- pipelines/simple.py +78 -0
- pipelines/simple_browser.py +145 -0
- pipelines/simple_chrome.py +75 -0
- pipelines/simple_notion.py +103 -0
- pipelines/tool_builder.py +0 -0
- zero_agent-0.1.0.dist-info/METADATA +269 -0
- zero_agent-0.1.0.dist-info/RECORD +66 -0
- zero_agent-0.1.0.dist-info/WHEEL +5 -0
- zero_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- zero_agent-0.1.0.dist-info/top_level.txt +2 -0
agentz/utils/helpers.py
ADDED
agentz/utils/parsers.py
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
"""
|
2
|
+
Output parsing utilities for JSON and structured data extraction.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from typing import Any, Callable, List
|
7
|
+
|
8
|
+
from pydantic import BaseModel
|
9
|
+
|
10
|
+
|
11
|
+
class OutputParserError(Exception):
|
12
|
+
"""
|
13
|
+
Exception raised when the output parser fails to parse the output.
|
14
|
+
"""
|
15
|
+
def __init__(self, message, output=None):
|
16
|
+
self.message = message
|
17
|
+
self.output = output
|
18
|
+
super().__init__(self.message)
|
19
|
+
|
20
|
+
def __str__(self):
|
21
|
+
if self.output:
|
22
|
+
return f"{self.message}\nProblematic output: {self.output}"
|
23
|
+
return self.message
|
24
|
+
|
25
|
+
|
26
|
+
def _escape_unescaped_quotes(json_text: str) -> str:
|
27
|
+
"""Escape bare double quotes that appear inside JSON string values."""
|
28
|
+
result: List[str] = []
|
29
|
+
in_string = False
|
30
|
+
escape_next = False
|
31
|
+
for index, char in enumerate(json_text):
|
32
|
+
if escape_next:
|
33
|
+
result.append(char)
|
34
|
+
escape_next = False
|
35
|
+
continue
|
36
|
+
if char == '\\':
|
37
|
+
result.append(char)
|
38
|
+
escape_next = True
|
39
|
+
continue
|
40
|
+
if char == '"':
|
41
|
+
if in_string:
|
42
|
+
lookahead = index + 1
|
43
|
+
while lookahead < len(json_text) and json_text[lookahead] in " \t\r\n":
|
44
|
+
lookahead += 1
|
45
|
+
if lookahead < len(json_text) and json_text[lookahead] not in ",}]":
|
46
|
+
result.append('\\"')
|
47
|
+
else:
|
48
|
+
result.append('"')
|
49
|
+
in_string = False
|
50
|
+
else:
|
51
|
+
in_string = True
|
52
|
+
result.append('"')
|
53
|
+
else:
|
54
|
+
result.append(char)
|
55
|
+
return "".join(result)
|
56
|
+
|
57
|
+
|
58
|
+
def find_json_in_string(string: str) -> str:
|
59
|
+
"""
|
60
|
+
Method to extract all text in the left-most brace that appears in a string.
|
61
|
+
Used to extract JSON from a string (note that this function does not validate the JSON).
|
62
|
+
|
63
|
+
Example:
|
64
|
+
string = "bla bla bla {this is {some} text{{}and it's sneaky}} because {it's} confusing"
|
65
|
+
output = "{this is {some} text{{}and it's sneaky}}"
|
66
|
+
"""
|
67
|
+
stack = 0
|
68
|
+
start_index = None
|
69
|
+
|
70
|
+
for i, c in enumerate(string):
|
71
|
+
if c == '{':
|
72
|
+
if stack == 0:
|
73
|
+
start_index = i # Start index of the first '{'
|
74
|
+
stack += 1 # Push to stack
|
75
|
+
elif c == '}':
|
76
|
+
stack -= 1 # Pop stack
|
77
|
+
if stack == 0:
|
78
|
+
# Return the substring from the start of the first '{' to the current '}'
|
79
|
+
return string[start_index:i + 1] if start_index is not None else ""
|
80
|
+
|
81
|
+
# If no complete set of braces is found, return an empty string
|
82
|
+
return ""
|
83
|
+
|
84
|
+
|
85
|
+
def parse_json_output(output: str) -> Any:
|
86
|
+
"""Take a string output and parse it as JSON"""
|
87
|
+
# First try to load the string as JSON
|
88
|
+
try:
|
89
|
+
return json.loads(output)
|
90
|
+
except json.JSONDecodeError as e:
|
91
|
+
escaped_output = _escape_unescaped_quotes(output)
|
92
|
+
if escaped_output != output:
|
93
|
+
try:
|
94
|
+
return json.loads(escaped_output)
|
95
|
+
except json.JSONDecodeError:
|
96
|
+
pass
|
97
|
+
pass
|
98
|
+
|
99
|
+
# If that fails, assume that the output is in a code block - remove the code block markers and try again
|
100
|
+
parsed_output = output
|
101
|
+
parsed_output = parsed_output.split("```")[1]
|
102
|
+
parsed_output = parsed_output.split("```")[0]
|
103
|
+
if parsed_output.startswith("json") or parsed_output.startswith("JSON"):
|
104
|
+
parsed_output = parsed_output[4:].strip()
|
105
|
+
try:
|
106
|
+
return json.loads(parsed_output)
|
107
|
+
except json.JSONDecodeError:
|
108
|
+
escaped_output = _escape_unescaped_quotes(parsed_output)
|
109
|
+
if escaped_output != parsed_output:
|
110
|
+
try:
|
111
|
+
return json.loads(escaped_output)
|
112
|
+
except json.JSONDecodeError:
|
113
|
+
pass
|
114
|
+
pass
|
115
|
+
|
116
|
+
# As a last attempt, try to manually find the JSON object in the output and parse it
|
117
|
+
parsed_output = find_json_in_string(output)
|
118
|
+
if parsed_output:
|
119
|
+
try:
|
120
|
+
return json.loads(parsed_output)
|
121
|
+
except json.JSONDecodeError:
|
122
|
+
escaped_output = _escape_unescaped_quotes(parsed_output)
|
123
|
+
if escaped_output != parsed_output:
|
124
|
+
try:
|
125
|
+
return json.loads(escaped_output)
|
126
|
+
except json.JSONDecodeError:
|
127
|
+
pass
|
128
|
+
raise OutputParserError(f"Failed to parse output as JSON", output)
|
129
|
+
|
130
|
+
# If all fails, raise an error
|
131
|
+
raise OutputParserError(f"Failed to parse output as JSON", output)
|
132
|
+
|
133
|
+
|
134
|
+
def create_type_parser(type: BaseModel) -> Callable[[str], BaseModel]:
|
135
|
+
"""Create a function that takes a string output and parses it as a specified Pydantic model"""
|
136
|
+
|
137
|
+
def convert_json_string_to_type(output: str) -> BaseModel:
|
138
|
+
"""Take a string output and parse it as a Pydantic model"""
|
139
|
+
output_dict = parse_json_output(output)
|
140
|
+
return type.model_validate(output_dict)
|
141
|
+
|
142
|
+
return convert_json_string_to_type
|
agentz/utils/printer.py
ADDED
@@ -0,0 +1,539 @@
|
|
1
|
+
"""
|
2
|
+
Rich-powered status printer for streaming pipeline progress updates.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import re
|
7
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
8
|
+
from collections import OrderedDict
|
9
|
+
|
10
|
+
from rich.console import Console, Group
|
11
|
+
from rich.live import Live
|
12
|
+
from rich.panel import Panel
|
13
|
+
from rich.markdown import Markdown
|
14
|
+
from rich.spinner import Spinner
|
15
|
+
from rich.syntax import Syntax
|
16
|
+
from rich.text import Text
|
17
|
+
|
18
|
+
|
19
|
+
class Printer:
|
20
|
+
"""Rich-powered status printer for streaming pipeline progress updates.
|
21
|
+
|
22
|
+
- Each item is displayed as a Panel (box) with a title.
|
23
|
+
- In-progress items show a spinner above the panel.
|
24
|
+
- Completed items show a checkmark in the panel title (unless hidden).
|
25
|
+
- Supports nested layout: groups (iterations) contain section panels.
|
26
|
+
- Per-section border colors with sensible defaults.
|
27
|
+
- Backward compatible with previous `update_item` signature.
|
28
|
+
"""
|
29
|
+
|
30
|
+
# Default border colors by section name
|
31
|
+
DEFAULT_BORDER_COLORS = {
|
32
|
+
"observations": "yellow",
|
33
|
+
"observation": "yellow",
|
34
|
+
"observe": "yellow",
|
35
|
+
"evaluation": "magenta",
|
36
|
+
"evaluate": "magenta",
|
37
|
+
"routing": "blue",
|
38
|
+
"route": "blue",
|
39
|
+
"tools": "cyan",
|
40
|
+
"tool": "cyan",
|
41
|
+
"writer": "green",
|
42
|
+
"write": "green",
|
43
|
+
}
|
44
|
+
|
45
|
+
def __init__(self, console: Console) -> None:
|
46
|
+
self.console = console
|
47
|
+
# You can tweak screen=True to prevent scrollback; kept False to preserve logs
|
48
|
+
self.live = Live(
|
49
|
+
console=console,
|
50
|
+
refresh_per_second=12,
|
51
|
+
vertical_overflow="ellipsis", # keep dashboard within viewport
|
52
|
+
screen=False, # keep using normal screen buffer
|
53
|
+
transient=True, # clear live view when stopping
|
54
|
+
)
|
55
|
+
|
56
|
+
# items: id -> (content, is_done, title, border_style, group_id)
|
57
|
+
self.items: Dict[str, Tuple[str, bool, Optional[str], Optional[str], Optional[str]]] = {}
|
58
|
+
# Track which items should hide the done checkmark
|
59
|
+
self.hide_done_ids: Set[str] = set()
|
60
|
+
|
61
|
+
# Rich content produced by agents per iteration (title -> panel)
|
62
|
+
self.iteration_sections: Dict[int, OrderedDict[str, Panel]] = {}
|
63
|
+
# Original content strings for creating previews (title -> content_string)
|
64
|
+
self.iteration_content: Dict[int, OrderedDict[str, str]] = {}
|
65
|
+
self.iteration_order: List[int] = []
|
66
|
+
self.finalized_iterations: Set[int] = set()
|
67
|
+
|
68
|
+
# Group management
|
69
|
+
self.group_order: List[str] = [] # Order of groups
|
70
|
+
self.groups: Dict[str, Dict[str, Any]] = {} # group_id -> {title, is_done, border_style, order}
|
71
|
+
self.item_order: List[str] = [] # Order of top-level items (no group_id)
|
72
|
+
|
73
|
+
self.live.start()
|
74
|
+
|
75
|
+
def end(self) -> None:
|
76
|
+
"""Stop the live rendering session."""
|
77
|
+
self.live.stop()
|
78
|
+
|
79
|
+
def hide_done_checkmark(self, item_id: str) -> None:
|
80
|
+
"""Hide the completion checkmark for the given item id."""
|
81
|
+
self.hide_done_ids.add(item_id)
|
82
|
+
|
83
|
+
def start_group(
|
84
|
+
self,
|
85
|
+
group_id: str,
|
86
|
+
*,
|
87
|
+
title: Optional[str] = None,
|
88
|
+
border_style: Optional[str] = None
|
89
|
+
) -> None:
|
90
|
+
"""Start a new group (e.g., an iteration panel).
|
91
|
+
|
92
|
+
Args:
|
93
|
+
group_id: Unique identifier for the group
|
94
|
+
title: Optional title for the group panel
|
95
|
+
border_style: Optional border color (defaults to white)
|
96
|
+
"""
|
97
|
+
if group_id not in self.groups:
|
98
|
+
self.group_order.append(group_id)
|
99
|
+
self.groups[group_id] = {
|
100
|
+
"title": title or group_id,
|
101
|
+
"is_done": False,
|
102
|
+
"border_style": border_style or "white",
|
103
|
+
"order": [] # Track order of items in this group
|
104
|
+
}
|
105
|
+
self._flush()
|
106
|
+
|
107
|
+
def end_group(
|
108
|
+
self,
|
109
|
+
group_id: str,
|
110
|
+
*,
|
111
|
+
is_done: bool = True,
|
112
|
+
title: Optional[str] = None
|
113
|
+
) -> None:
|
114
|
+
"""Mark a group as complete.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
group_id: Unique identifier for the group
|
118
|
+
is_done: Whether the group is complete (default: True)
|
119
|
+
title: Optional updated title for the group
|
120
|
+
"""
|
121
|
+
if group_id in self.groups:
|
122
|
+
self.groups[group_id]["is_done"] = is_done
|
123
|
+
if title:
|
124
|
+
self.groups[group_id]["title"] = title
|
125
|
+
# Update border style to bright_white when done
|
126
|
+
if is_done and self.groups[group_id]["border_style"] == "white":
|
127
|
+
self.groups[group_id]["border_style"] = "bright_white"
|
128
|
+
|
129
|
+
iteration = self._extract_iteration_index(group_id)
|
130
|
+
if iteration is not None and is_done:
|
131
|
+
self._finalize_iteration(iteration)
|
132
|
+
else:
|
133
|
+
self._flush()
|
134
|
+
|
135
|
+
def update_item(
|
136
|
+
self,
|
137
|
+
item_id: str,
|
138
|
+
content: str,
|
139
|
+
*,
|
140
|
+
is_done: bool = False,
|
141
|
+
hide_checkmark: bool = False,
|
142
|
+
title: Optional[str] = None,
|
143
|
+
border_style: Optional[str] = None,
|
144
|
+
group_id: Optional[str] = None,
|
145
|
+
) -> None:
|
146
|
+
"""Insert or update a status line and refresh the live console.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
item_id: Unique identifier for the item
|
150
|
+
content: Content to display
|
151
|
+
is_done: Whether the task is complete
|
152
|
+
hide_checkmark: Hide completion checkmark
|
153
|
+
title: Optional panel title
|
154
|
+
border_style: Optional border color (auto-detected from title if not provided)
|
155
|
+
group_id: Optional group to nest this item in
|
156
|
+
"""
|
157
|
+
# Auto-detect border style from title if not provided
|
158
|
+
if border_style is None and title:
|
159
|
+
title_lower = title.lower().strip()
|
160
|
+
# Check for exact match first
|
161
|
+
if title_lower in self.DEFAULT_BORDER_COLORS:
|
162
|
+
border_style = self.DEFAULT_BORDER_COLORS[title_lower]
|
163
|
+
else:
|
164
|
+
# Check if any key is a substring of the title
|
165
|
+
for key, color in self.DEFAULT_BORDER_COLORS.items():
|
166
|
+
if key in title_lower:
|
167
|
+
border_style = color
|
168
|
+
break
|
169
|
+
|
170
|
+
# Track item in appropriate order list
|
171
|
+
if item_id not in self.items:
|
172
|
+
if group_id and group_id in self.groups:
|
173
|
+
if item_id not in self.groups[group_id]["order"]:
|
174
|
+
self.groups[group_id]["order"].append(item_id)
|
175
|
+
elif item_id not in self.item_order:
|
176
|
+
self.item_order.append(item_id)
|
177
|
+
|
178
|
+
self.items[item_id] = (content, is_done, title, border_style, group_id)
|
179
|
+
if hide_checkmark:
|
180
|
+
self.hide_done_ids.add(item_id)
|
181
|
+
self._flush()
|
182
|
+
|
183
|
+
def mark_item_done(
|
184
|
+
self,
|
185
|
+
item_id: str,
|
186
|
+
*,
|
187
|
+
title: Optional[str] = None,
|
188
|
+
border_style: Optional[str] = None
|
189
|
+
) -> None:
|
190
|
+
"""Mark an existing status line as completed (optionally update title/border).
|
191
|
+
|
192
|
+
Args:
|
193
|
+
item_id: Unique identifier for the item
|
194
|
+
title: Optional updated title
|
195
|
+
border_style: Optional updated border color
|
196
|
+
"""
|
197
|
+
if item_id in self.items:
|
198
|
+
content, _, old_title, old_border, group_id = self.items[item_id]
|
199
|
+
self.items[item_id] = (
|
200
|
+
content,
|
201
|
+
True,
|
202
|
+
title or old_title,
|
203
|
+
border_style or old_border,
|
204
|
+
group_id
|
205
|
+
)
|
206
|
+
self._flush()
|
207
|
+
|
208
|
+
def log_panel(
|
209
|
+
self,
|
210
|
+
title: str,
|
211
|
+
content: str,
|
212
|
+
*,
|
213
|
+
border_style: Optional[str] = None,
|
214
|
+
iteration: Optional[int] = None,
|
215
|
+
) -> None:
|
216
|
+
"""Render a standalone panel outside the live dashboard.
|
217
|
+
|
218
|
+
Useful for persisting rich text output to the terminal while keeping the
|
219
|
+
live printer focused on lightweight status updates.
|
220
|
+
"""
|
221
|
+
# content = self._truncate_content(content)
|
222
|
+
|
223
|
+
if border_style is None:
|
224
|
+
title_lower = title.lower().strip()
|
225
|
+
if title_lower in self.DEFAULT_BORDER_COLORS:
|
226
|
+
border_style = self.DEFAULT_BORDER_COLORS[title_lower]
|
227
|
+
else:
|
228
|
+
for key, color in self.DEFAULT_BORDER_COLORS.items():
|
229
|
+
if key in title_lower:
|
230
|
+
border_style = color
|
231
|
+
break
|
232
|
+
|
233
|
+
panel = Panel(
|
234
|
+
self._detect_and_render_body(content),
|
235
|
+
title=Text(title),
|
236
|
+
border_style=border_style or "cyan",
|
237
|
+
padding=(1, 2),
|
238
|
+
expand=True,
|
239
|
+
)
|
240
|
+
|
241
|
+
if iteration is not None:
|
242
|
+
sections = self.iteration_sections.setdefault(iteration, OrderedDict())
|
243
|
+
content_dict = self.iteration_content.setdefault(iteration, OrderedDict())
|
244
|
+
if iteration not in self.iteration_order:
|
245
|
+
self.iteration_order.append(iteration)
|
246
|
+
sections[title] = panel
|
247
|
+
content_dict[title] = content # Store original content for previews
|
248
|
+
if iteration in self.finalized_iterations:
|
249
|
+
self.finalized_iterations.discard(iteration)
|
250
|
+
self._flush()
|
251
|
+
else:
|
252
|
+
self.live.console.print(panel)
|
253
|
+
|
254
|
+
# ------------ internals ------------
|
255
|
+
|
256
|
+
def _truncate_content(self, content: str) -> str:
|
257
|
+
"""Limit panel body length so it fits comfortably in a terminal window."""
|
258
|
+
max_lines = 40
|
259
|
+
max_cols = 120
|
260
|
+
lines = content.splitlines()
|
261
|
+
truncated: List[str] = []
|
262
|
+
|
263
|
+
for idx, line in enumerate(lines):
|
264
|
+
shortened = line
|
265
|
+
if len(shortened) > max_cols:
|
266
|
+
shortened = shortened[: max_cols - 1].rstrip() + "…"
|
267
|
+
truncated.append(shortened)
|
268
|
+
if idx + 1 >= max_lines:
|
269
|
+
if idx + 1 < len(lines):
|
270
|
+
truncated.append("…")
|
271
|
+
break
|
272
|
+
|
273
|
+
result = "\n".join(truncated)
|
274
|
+
|
275
|
+
# Ensure code fences remain balanced after truncation
|
276
|
+
if result.count("```") % 2 == 1:
|
277
|
+
result += "\n```"
|
278
|
+
return result
|
279
|
+
|
280
|
+
def _detect_and_render_body(self, content: str) -> Any:
|
281
|
+
"""Auto-detect content type and render with appropriate Rich object.
|
282
|
+
|
283
|
+
Detection order:
|
284
|
+
1. ANSI escape codes → Text.from_ansi
|
285
|
+
2. Rich markup (e.g., [bold cyan]...[/]) → Text.from_markup
|
286
|
+
3. JSON → Syntax highlighting
|
287
|
+
4. Code patterns → Syntax highlighting
|
288
|
+
5. Markdown (headers, bold, bullets, code fences) → Markdown
|
289
|
+
6. Plain text → Text
|
290
|
+
"""
|
291
|
+
# Check for ANSI escape codes
|
292
|
+
ansi_pattern = r'\x1b\[[0-9;]*m'
|
293
|
+
if re.search(ansi_pattern, content):
|
294
|
+
return Text.from_ansi(content)
|
295
|
+
|
296
|
+
# Check for Rich markup patterns
|
297
|
+
rich_markup_pattern = r'\[/?[a-z_]+(?:\s+[a-z_]+)*\]'
|
298
|
+
if re.search(rich_markup_pattern, content, re.IGNORECASE):
|
299
|
+
return Text.from_markup(content, emoji=True)
|
300
|
+
|
301
|
+
# Check for JSON (starts with { or [, ends with } or ])
|
302
|
+
stripped = content.strip()
|
303
|
+
if (stripped.startswith('{') and stripped.endswith('}')) or \
|
304
|
+
(stripped.startswith('[') and stripped.endswith(']')):
|
305
|
+
try:
|
306
|
+
json.loads(stripped) # Validate it's valid JSON
|
307
|
+
return Syntax(content, "json", theme="monokai", line_numbers=False)
|
308
|
+
except (json.JSONDecodeError, ValueError):
|
309
|
+
pass
|
310
|
+
|
311
|
+
# Check for common code patterns (imports, function definitions, etc.)
|
312
|
+
code_patterns = [
|
313
|
+
r'^\s*import\s+',
|
314
|
+
r'^\s*from\s+.+\s+import\s+',
|
315
|
+
r'^\s*def\s+\w+\s*\(',
|
316
|
+
r'^\s*class\s+\w+',
|
317
|
+
r'^\s*async\s+def\s+',
|
318
|
+
r'^\s*@\w+',
|
319
|
+
r'^\s*if\s+__name__\s*==',
|
320
|
+
]
|
321
|
+
|
322
|
+
for pattern in code_patterns:
|
323
|
+
if re.search(pattern, content, re.MULTILINE):
|
324
|
+
# Detect language
|
325
|
+
if re.search(r'^\s*(import|from|def|class|async)', content, re.MULTILINE):
|
326
|
+
return Syntax(content, "python", theme="monokai", line_numbers=False)
|
327
|
+
break
|
328
|
+
|
329
|
+
# Check for markdown patterns (more aggressive detection)
|
330
|
+
markdown_patterns = [
|
331
|
+
r'^\s*#{1,6}\s+', # Headers
|
332
|
+
r'\*\*[^*]+\*\*', # Bold
|
333
|
+
r'\*[^*]+\*', # Italic
|
334
|
+
r'^\s*[\*\-\+]\s+', # Unordered lists
|
335
|
+
r'^\s*\d+\.\s+', # Ordered lists
|
336
|
+
r'```', # Code fences
|
337
|
+
r'\[.+\]\(.+\)', # Links
|
338
|
+
r'^\s*>\s+', # Blockquotes
|
339
|
+
]
|
340
|
+
|
341
|
+
for pattern in markdown_patterns:
|
342
|
+
if re.search(pattern, content, re.MULTILINE):
|
343
|
+
return Markdown(content, code_theme="monokai", inline_code_theme="monokai")
|
344
|
+
|
345
|
+
# Default to plain text
|
346
|
+
return Text(content)
|
347
|
+
|
348
|
+
def _extract_iteration_index(self, group_id: str) -> Optional[int]:
|
349
|
+
match = re.fullmatch(r"iter-(\d+)", group_id)
|
350
|
+
if match:
|
351
|
+
return int(match.group(1))
|
352
|
+
return None
|
353
|
+
|
354
|
+
def _build_iteration_panel(self, iteration: int) -> Optional[Panel]:
|
355
|
+
sections = self.iteration_sections.get(iteration)
|
356
|
+
if not sections:
|
357
|
+
return None
|
358
|
+
section_group = Group(*sections.values())
|
359
|
+
return Panel(
|
360
|
+
section_group,
|
361
|
+
title=Text(f"Iteration {iteration}"),
|
362
|
+
border_style="bright_white",
|
363
|
+
padding=1,
|
364
|
+
expand=True,
|
365
|
+
)
|
366
|
+
|
367
|
+
def _build_activity_preview_panel(self, iteration: int) -> Optional[Panel]:
|
368
|
+
"""Build a truncated preview panel showing only the current agent output.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
iteration: The iteration number to preview
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
A Panel with truncated preview of the most recent agent output, or None if no content
|
375
|
+
"""
|
376
|
+
content_dict = self.iteration_content.get(iteration)
|
377
|
+
if not content_dict:
|
378
|
+
return None
|
379
|
+
|
380
|
+
# Get only the most recent section (last item in OrderedDict)
|
381
|
+
last_title = None
|
382
|
+
last_content = None
|
383
|
+
for title, content in content_dict.items():
|
384
|
+
last_title = title
|
385
|
+
last_content = content
|
386
|
+
|
387
|
+
if not last_title or not last_content:
|
388
|
+
return None
|
389
|
+
|
390
|
+
# Truncate to first N lines and max characters to prevent long scrolling
|
391
|
+
max_preview_lines = 12
|
392
|
+
max_preview_chars = 2000
|
393
|
+
lines = last_content.splitlines()
|
394
|
+
preview_text = "\n".join(lines[:max_preview_lines])
|
395
|
+
if len(lines) > max_preview_lines:
|
396
|
+
preview_text += "\n..."
|
397
|
+
|
398
|
+
# Also truncate by character count
|
399
|
+
if len(preview_text) > max_preview_chars:
|
400
|
+
preview_text = preview_text[:max_preview_chars] + "\n..."
|
401
|
+
|
402
|
+
# Render the content using the same detection logic as log_panel
|
403
|
+
preview_renderable = self._detect_and_render_body(preview_text)
|
404
|
+
|
405
|
+
return Panel(
|
406
|
+
preview_renderable,
|
407
|
+
title=Text(f"Current Activity: {last_title}", style="bold cyan"),
|
408
|
+
border_style="bright_black",
|
409
|
+
padding=1,
|
410
|
+
expand=True,
|
411
|
+
)
|
412
|
+
|
413
|
+
def _finalize_iteration(self, iteration: int) -> None:
|
414
|
+
if iteration in self.finalized_iterations:
|
415
|
+
return
|
416
|
+
panel = self._build_iteration_panel(iteration)
|
417
|
+
if panel:
|
418
|
+
self.console.print(panel)
|
419
|
+
self.finalized_iterations.add(iteration)
|
420
|
+
self.iteration_sections.pop(iteration, None)
|
421
|
+
self.iteration_content.pop(iteration, None) # Also clean up content storage
|
422
|
+
if iteration in self.iteration_order:
|
423
|
+
self.iteration_order.remove(iteration)
|
424
|
+
self._flush()
|
425
|
+
|
426
|
+
def _render_item(
|
427
|
+
self,
|
428
|
+
item_id: str,
|
429
|
+
content: str,
|
430
|
+
is_done: bool,
|
431
|
+
title: Optional[str],
|
432
|
+
*,
|
433
|
+
indent: int = 0,
|
434
|
+
) -> Any:
|
435
|
+
"""Render a single status line without surrounding boxes."""
|
436
|
+
prefix = "✓" if is_done and item_id not in self.hide_done_ids else ("•" if is_done else "…")
|
437
|
+
indent_str = " " * (indent * 2)
|
438
|
+
lines = str(content).splitlines() or [""]
|
439
|
+
|
440
|
+
label = title or item_id
|
441
|
+
primary = lines[0].strip()
|
442
|
+
if label and primary:
|
443
|
+
display_text = f"{label}: {primary}"
|
444
|
+
elif label:
|
445
|
+
display_text = label
|
446
|
+
elif primary:
|
447
|
+
display_text = primary
|
448
|
+
else:
|
449
|
+
display_text = title or item_id or ""
|
450
|
+
|
451
|
+
if not is_done:
|
452
|
+
spinner_text = Text(f"{indent_str}{display_text}")
|
453
|
+
spinner = Spinner("dots", text=spinner_text)
|
454
|
+
if len(lines) == 1:
|
455
|
+
return spinner
|
456
|
+
continuation = [Text(f"{indent_str} {line}") for line in lines[1:]]
|
457
|
+
return Group(spinner, *continuation)
|
458
|
+
|
459
|
+
headline = f"{indent_str}{prefix} {display_text}".rstrip()
|
460
|
+
if not display_text:
|
461
|
+
headline = f"{indent_str}{prefix}"
|
462
|
+
first_line = Text(headline)
|
463
|
+
if len(lines) == 1:
|
464
|
+
return first_line
|
465
|
+
continuation = [Text(f"{indent_str} {line}") for line in lines[1:]]
|
466
|
+
return Group(first_line, *continuation)
|
467
|
+
|
468
|
+
def _render_group(self, group_id: str) -> Any:
|
469
|
+
"""Render a group panel containing its child section panels.
|
470
|
+
|
471
|
+
Args:
|
472
|
+
group_id: The group to render
|
473
|
+
|
474
|
+
Returns:
|
475
|
+
A Panel containing Group of child panels
|
476
|
+
"""
|
477
|
+
group = self.groups[group_id]
|
478
|
+
group_items: List[Any] = []
|
479
|
+
|
480
|
+
# Render items in the order they were added to this group
|
481
|
+
for item_id in group["order"]:
|
482
|
+
if item_id in self.items:
|
483
|
+
content, is_done, title, _border_style, _ = self.items[item_id]
|
484
|
+
group_items.append(
|
485
|
+
self._render_item(item_id, content, is_done, title, indent=1)
|
486
|
+
)
|
487
|
+
|
488
|
+
status_symbol = "✓" if group["is_done"] else "…"
|
489
|
+
header = Text(f"{status_symbol} {group['title']}")
|
490
|
+
return Group(header, *(group_items or [Text(" (no activity)")]))
|
491
|
+
|
492
|
+
def _flush(self) -> None:
|
493
|
+
"""Re-render the live view with the latest status items."""
|
494
|
+
renderables: List[Any] = []
|
495
|
+
|
496
|
+
# Render top-level items (those without group_id)
|
497
|
+
for item_id in self.item_order:
|
498
|
+
if item_id in self.items:
|
499
|
+
content, is_done, title, _border_style, group_id = self.items[item_id]
|
500
|
+
if group_id is None:
|
501
|
+
renderables.append(
|
502
|
+
self._render_item(item_id, content, is_done, title)
|
503
|
+
)
|
504
|
+
|
505
|
+
# Render groups in order
|
506
|
+
for group_id in self.group_order:
|
507
|
+
if group_id in self.groups:
|
508
|
+
renderables.append(self._render_group(group_id))
|
509
|
+
|
510
|
+
render_groups: List[Any] = []
|
511
|
+
|
512
|
+
# Status panel containing live progress lines
|
513
|
+
status_body = Group(*renderables) if renderables else Text("No status yet")
|
514
|
+
status_panel = Panel(
|
515
|
+
status_body,
|
516
|
+
title=Text("Status", style="bold"),
|
517
|
+
border_style="bright_black",
|
518
|
+
padding=(0, 1),
|
519
|
+
expand=True,
|
520
|
+
)
|
521
|
+
render_groups.append(status_panel)
|
522
|
+
|
523
|
+
# Show only the latest active iteration inside the live view
|
524
|
+
active_iterations = [
|
525
|
+
iteration
|
526
|
+
for iteration in self.iteration_order
|
527
|
+
if iteration not in self.finalized_iterations
|
528
|
+
and self.iteration_sections.get(iteration)
|
529
|
+
]
|
530
|
+
if active_iterations:
|
531
|
+
current_iteration = max(active_iterations)
|
532
|
+
|
533
|
+
# Add truncated preview panel for current activity only
|
534
|
+
# (Full iteration panels are shown when finalized via _finalize_iteration)
|
535
|
+
preview_panel = self._build_activity_preview_panel(current_iteration)
|
536
|
+
if preview_panel:
|
537
|
+
render_groups.append(preview_panel)
|
538
|
+
|
539
|
+
self.live.update(Group(*render_groups))
|