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
@@ -0,0 +1,538 @@
|
|
1
|
+
"""ArtifactWriter persists run data to markdown and HTML files."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import json
|
6
|
+
import time
|
7
|
+
from datetime import datetime
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from agentz.artifacts.reporter import AgentStepRecord, PanelRecord
|
13
|
+
|
14
|
+
|
15
|
+
def _utc_timestamp() -> str:
|
16
|
+
"""Return current UTC timestamp with second precision."""
|
17
|
+
return datetime.utcnow().replace(tzinfo=None).isoformat(timespec="seconds") + "Z"
|
18
|
+
|
19
|
+
|
20
|
+
def _json_default(obj: Any) -> Any:
|
21
|
+
"""Fallback JSON serialiser for arbitrary objects."""
|
22
|
+
if hasattr(obj, "model_dump"):
|
23
|
+
return obj.model_dump()
|
24
|
+
if hasattr(obj, "__dict__"):
|
25
|
+
return obj.__dict__
|
26
|
+
return str(obj)
|
27
|
+
|
28
|
+
|
29
|
+
class ArtifactWriter:
|
30
|
+
"""Collects run data and persists it as markdown and HTML artifacts."""
|
31
|
+
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
*,
|
35
|
+
base_dir: Path,
|
36
|
+
pipeline_slug: str,
|
37
|
+
workflow_name: str,
|
38
|
+
experiment_id: str,
|
39
|
+
) -> None:
|
40
|
+
self.base_dir = base_dir
|
41
|
+
self.pipeline_slug = pipeline_slug
|
42
|
+
self.workflow_name = workflow_name
|
43
|
+
self.experiment_id = experiment_id
|
44
|
+
|
45
|
+
self.run_dir = base_dir / pipeline_slug / experiment_id
|
46
|
+
self.terminal_md_path = self.run_dir / "terminal_log.md"
|
47
|
+
self.terminal_html_path = self.run_dir / "terminal_log.html"
|
48
|
+
self.final_report_md_path = self.run_dir / "final_report.md"
|
49
|
+
self.final_report_html_path = self.run_dir / "final_report.html"
|
50
|
+
|
51
|
+
self._panels: List[PanelRecord] = []
|
52
|
+
self._agent_steps: Dict[str, AgentStepRecord] = {}
|
53
|
+
self._groups: Dict[str, Dict[str, Any]] = {}
|
54
|
+
self._iterations: Dict[str, Dict[str, Any]] = {}
|
55
|
+
self._final_result: Optional[Any] = None
|
56
|
+
|
57
|
+
self._start_time: Optional[float] = None
|
58
|
+
self._started_at_iso: Optional[str] = None
|
59
|
+
self._finished_at_iso: Optional[str] = None
|
60
|
+
|
61
|
+
# ------------------------------------------------------------------ basics
|
62
|
+
|
63
|
+
def start(self, config: Any) -> None: # noqa: ARG002 - config reserved for future use
|
64
|
+
"""Prepare filesystem layout and capture start metadata."""
|
65
|
+
if self._start_time is not None:
|
66
|
+
return
|
67
|
+
self.run_dir.mkdir(parents=True, exist_ok=True)
|
68
|
+
self._start_time = time.time()
|
69
|
+
self._started_at_iso = _utc_timestamp()
|
70
|
+
|
71
|
+
def set_final_result(self, result: Any) -> None:
|
72
|
+
"""Store pipeline result for later persistence."""
|
73
|
+
self._final_result = result
|
74
|
+
|
75
|
+
# ----------------------------------------------------------------- logging
|
76
|
+
|
77
|
+
def record_status_update(
|
78
|
+
self,
|
79
|
+
*,
|
80
|
+
item_id: str,
|
81
|
+
content: str,
|
82
|
+
is_done: bool,
|
83
|
+
title: Optional[str],
|
84
|
+
border_style: Optional[str],
|
85
|
+
group_id: Optional[str],
|
86
|
+
) -> None: # noqa: D401 - keeps signature compatibility
|
87
|
+
"""Currently unused; maintained for interface compatibility."""
|
88
|
+
# Intentionally no-op for the simplified reporter.
|
89
|
+
return None
|
90
|
+
|
91
|
+
def record_group_start(
|
92
|
+
self,
|
93
|
+
*,
|
94
|
+
group_id: str,
|
95
|
+
title: Optional[str],
|
96
|
+
border_style: Optional[str],
|
97
|
+
iteration: Optional[int] = None,
|
98
|
+
) -> None:
|
99
|
+
"""Record the start of an iteration/group."""
|
100
|
+
timestamp = _utc_timestamp()
|
101
|
+
payload = {
|
102
|
+
"group_id": group_id,
|
103
|
+
"title": title,
|
104
|
+
"border_style": border_style,
|
105
|
+
"iteration": iteration,
|
106
|
+
"started_at": timestamp,
|
107
|
+
}
|
108
|
+
self._groups[group_id] = payload
|
109
|
+
if iteration is not None:
|
110
|
+
iter_key = f"iter-{iteration}"
|
111
|
+
self._iterations.setdefault(
|
112
|
+
iter_key,
|
113
|
+
{
|
114
|
+
"iteration": iteration,
|
115
|
+
"title": title or f"Iteration {iteration}",
|
116
|
+
"started_at": timestamp,
|
117
|
+
"finished_at": None,
|
118
|
+
"panels": [],
|
119
|
+
"agent_steps": [],
|
120
|
+
},
|
121
|
+
)
|
122
|
+
|
123
|
+
def record_group_end(
|
124
|
+
self,
|
125
|
+
*,
|
126
|
+
group_id: str,
|
127
|
+
is_done: bool = True,
|
128
|
+
title: Optional[str] = None,
|
129
|
+
) -> None:
|
130
|
+
"""Record the end of an iteration/group."""
|
131
|
+
timestamp = _utc_timestamp()
|
132
|
+
group_meta = self._groups.get(group_id)
|
133
|
+
if not group_meta:
|
134
|
+
return
|
135
|
+
group_meta.update(
|
136
|
+
{
|
137
|
+
"title": title or group_meta.get("title"),
|
138
|
+
"is_done": is_done,
|
139
|
+
"finished_at": timestamp,
|
140
|
+
}
|
141
|
+
)
|
142
|
+
iteration = group_meta.get("iteration")
|
143
|
+
if iteration is not None:
|
144
|
+
iter_key = f"iter-{iteration}"
|
145
|
+
iteration_meta = self._iterations.setdefault(
|
146
|
+
iter_key,
|
147
|
+
{
|
148
|
+
"iteration": iteration,
|
149
|
+
"title": title or f"Iteration {iteration}",
|
150
|
+
"panels": [],
|
151
|
+
"agent_steps": [],
|
152
|
+
},
|
153
|
+
)
|
154
|
+
iteration_meta["finished_at"] = timestamp
|
155
|
+
|
156
|
+
def record_agent_step_start(
|
157
|
+
self,
|
158
|
+
*,
|
159
|
+
step_id: str,
|
160
|
+
agent_name: str,
|
161
|
+
span_name: str,
|
162
|
+
iteration: Optional[int],
|
163
|
+
group_id: Optional[str],
|
164
|
+
printer_title: Optional[str],
|
165
|
+
) -> None:
|
166
|
+
"""Capture metadata when an agent step begins."""
|
167
|
+
from agentz.artifacts.reporter import AgentStepRecord
|
168
|
+
|
169
|
+
record = AgentStepRecord(
|
170
|
+
agent_name=agent_name,
|
171
|
+
span_name=span_name,
|
172
|
+
iteration=iteration,
|
173
|
+
group_id=group_id,
|
174
|
+
started_at=_utc_timestamp(),
|
175
|
+
)
|
176
|
+
self._agent_steps[step_id] = record
|
177
|
+
if iteration is not None:
|
178
|
+
iter_key = f"iter-{iteration}"
|
179
|
+
iteration_meta = self._iterations.setdefault(
|
180
|
+
iter_key,
|
181
|
+
{
|
182
|
+
"iteration": iteration,
|
183
|
+
"title": printer_title or f"Iteration {iteration}",
|
184
|
+
"panels": [],
|
185
|
+
"agent_steps": [],
|
186
|
+
},
|
187
|
+
)
|
188
|
+
iteration_meta["agent_steps"].append(record)
|
189
|
+
|
190
|
+
def record_agent_step_end(
|
191
|
+
self,
|
192
|
+
*,
|
193
|
+
step_id: str,
|
194
|
+
status: str,
|
195
|
+
duration_seconds: float,
|
196
|
+
error: Optional[str] = None,
|
197
|
+
) -> None:
|
198
|
+
"""Update agent step telemetry on completion."""
|
199
|
+
timestamp = _utc_timestamp()
|
200
|
+
record = self._agent_steps.get(step_id)
|
201
|
+
if record:
|
202
|
+
record.finished_at = timestamp
|
203
|
+
record.duration_seconds = round(duration_seconds, 3)
|
204
|
+
record.status = status
|
205
|
+
record.error = error
|
206
|
+
|
207
|
+
def record_panel(
|
208
|
+
self,
|
209
|
+
*,
|
210
|
+
title: str,
|
211
|
+
content: str,
|
212
|
+
border_style: Optional[str],
|
213
|
+
iteration: Optional[int],
|
214
|
+
group_id: Optional[str],
|
215
|
+
) -> None:
|
216
|
+
"""Persist panel meta for terminal & HTML artefacts."""
|
217
|
+
from agentz.artifacts.reporter import PanelRecord
|
218
|
+
|
219
|
+
record = PanelRecord(
|
220
|
+
title=title,
|
221
|
+
content=content,
|
222
|
+
border_style=border_style,
|
223
|
+
iteration=iteration,
|
224
|
+
group_id=group_id,
|
225
|
+
recorded_at=_utc_timestamp(),
|
226
|
+
)
|
227
|
+
self._panels.append(record)
|
228
|
+
if iteration is not None:
|
229
|
+
iter_key = f"iter-{iteration}"
|
230
|
+
iteration_meta = self._iterations.setdefault(
|
231
|
+
iter_key,
|
232
|
+
{
|
233
|
+
"iteration": iteration,
|
234
|
+
"title": f"Iteration {iteration}",
|
235
|
+
"panels": [],
|
236
|
+
"agent_steps": [],
|
237
|
+
},
|
238
|
+
)
|
239
|
+
iteration_meta["panels"].append(record)
|
240
|
+
|
241
|
+
# ------------------------------------------------------------- finalisation
|
242
|
+
|
243
|
+
def finalize(self) -> None:
|
244
|
+
"""Persist markdown + HTML artefacts."""
|
245
|
+
if self._start_time is None or self._finished_at_iso is not None:
|
246
|
+
return
|
247
|
+
self._finished_at_iso = _utc_timestamp()
|
248
|
+
duration = round(time.time() - self._start_time, 3)
|
249
|
+
|
250
|
+
terminal_sections = self._build_terminal_sections()
|
251
|
+
terminal_md = self._render_terminal_markdown(duration, terminal_sections)
|
252
|
+
terminal_html = self._render_terminal_html(duration, terminal_sections)
|
253
|
+
|
254
|
+
self.terminal_md_path.write_text(terminal_md, encoding="utf-8")
|
255
|
+
self.terminal_html_path.write_text(terminal_html, encoding="utf-8")
|
256
|
+
|
257
|
+
final_md, final_html = self._render_final_report()
|
258
|
+
self.final_report_md_path.write_text(final_md, encoding="utf-8")
|
259
|
+
self.final_report_html_path.write_text(final_html, encoding="utf-8")
|
260
|
+
|
261
|
+
def _build_terminal_sections(self) -> List[Dict[str, Any]]:
|
262
|
+
"""Collect ordered sections for terminal artefacts."""
|
263
|
+
sections: List[Dict[str, Any]] = []
|
264
|
+
|
265
|
+
# Iteration/scoped panels
|
266
|
+
for iter_key, meta in sorted(
|
267
|
+
self._iterations.items(),
|
268
|
+
key=lambda item: item[1].get("iteration", 0),
|
269
|
+
):
|
270
|
+
sections.append(
|
271
|
+
{
|
272
|
+
"title": meta.get("title") or iter_key,
|
273
|
+
"started_at": meta.get("started_at"),
|
274
|
+
"finished_at": meta.get("finished_at"),
|
275
|
+
"panels": meta.get("panels", []),
|
276
|
+
"agent_steps": meta.get("agent_steps", []),
|
277
|
+
}
|
278
|
+
)
|
279
|
+
|
280
|
+
# Global panels (no iteration)
|
281
|
+
global_panels = [
|
282
|
+
record
|
283
|
+
for record in self._panels
|
284
|
+
if record.iteration is None
|
285
|
+
]
|
286
|
+
if global_panels:
|
287
|
+
sections.append(
|
288
|
+
{
|
289
|
+
"title": "General",
|
290
|
+
"started_at": None,
|
291
|
+
"finished_at": None,
|
292
|
+
"panels": global_panels,
|
293
|
+
"agent_steps": [],
|
294
|
+
}
|
295
|
+
)
|
296
|
+
|
297
|
+
return sections
|
298
|
+
|
299
|
+
def _render_terminal_markdown(
|
300
|
+
self,
|
301
|
+
duration: float,
|
302
|
+
sections: List[Dict[str, Any]],
|
303
|
+
) -> str:
|
304
|
+
"""Render the terminal log as Markdown."""
|
305
|
+
lines: List[str] = []
|
306
|
+
lines.append(f"# Terminal Log · {self.workflow_name}")
|
307
|
+
lines.append("")
|
308
|
+
lines.append(f"- **Experiment ID:** `{self.experiment_id}`")
|
309
|
+
lines.append(f"- **Started:** {self._started_at_iso or '–'}")
|
310
|
+
lines.append(f"- **Finished:** {self._finished_at_iso or '–'}")
|
311
|
+
lines.append(f"- **Duration:** {duration} seconds")
|
312
|
+
lines.append("")
|
313
|
+
|
314
|
+
if not sections:
|
315
|
+
lines.append("_No panels recorded during this run._")
|
316
|
+
lines.append("")
|
317
|
+
return "\n".join(lines)
|
318
|
+
|
319
|
+
for section in sections:
|
320
|
+
lines.append(f"## {section['title']}")
|
321
|
+
span = ""
|
322
|
+
if section.get("started_at") or section.get("finished_at"):
|
323
|
+
span = f"{section.get('started_at', '–')} → {section.get('finished_at', '–')}"
|
324
|
+
if span:
|
325
|
+
lines.append(f"*Time:* {span}")
|
326
|
+
lines.append("")
|
327
|
+
|
328
|
+
agent_steps: List[AgentStepRecord] = section.get("agent_steps", [])
|
329
|
+
if agent_steps:
|
330
|
+
lines.append("### Agent Steps")
|
331
|
+
for step in agent_steps:
|
332
|
+
duration_txt = (
|
333
|
+
f"{step.duration_seconds}s"
|
334
|
+
if step.duration_seconds is not None
|
335
|
+
else "pending"
|
336
|
+
)
|
337
|
+
status = step.status
|
338
|
+
error = f" · Error: {step.error}" if step.error else ""
|
339
|
+
lines.append(
|
340
|
+
f"- **{step.agent_name}** · {step.span_name} "
|
341
|
+
f"({duration_txt}) · {status}{error}"
|
342
|
+
)
|
343
|
+
lines.append("")
|
344
|
+
|
345
|
+
panels: List[PanelRecord] = section.get("panels", [])
|
346
|
+
for panel in panels:
|
347
|
+
panel_title = panel.title or "Panel"
|
348
|
+
lines.append(f"### {panel_title}")
|
349
|
+
lines.append("")
|
350
|
+
lines.append("```")
|
351
|
+
lines.append(panel.content.rstrip())
|
352
|
+
lines.append("```")
|
353
|
+
lines.append("")
|
354
|
+
|
355
|
+
return "\n".join(lines).rstrip() + "\n"
|
356
|
+
|
357
|
+
def _render_terminal_html(
|
358
|
+
self,
|
359
|
+
duration: float,
|
360
|
+
sections: List[Dict[str, Any]],
|
361
|
+
) -> str:
|
362
|
+
"""Render the terminal log as standalone HTML."""
|
363
|
+
body_sections: List[str] = []
|
364
|
+
|
365
|
+
for section in sections:
|
366
|
+
panels_html: List[str] = []
|
367
|
+
for panel in section.get("panels", []):
|
368
|
+
panel_html = f"""
|
369
|
+
<article class="panel">
|
370
|
+
<h3>{panel.title or "Panel"}</h3>
|
371
|
+
<pre>{panel.content}</pre>
|
372
|
+
</article>
|
373
|
+
""".strip()
|
374
|
+
panels_html.append(panel_html)
|
375
|
+
|
376
|
+
agent_html: List[str] = []
|
377
|
+
for step in section.get("agent_steps", []):
|
378
|
+
info = json.dumps(
|
379
|
+
{
|
380
|
+
"agent": step.agent_name,
|
381
|
+
"span": step.span_name,
|
382
|
+
"status": step.status,
|
383
|
+
"duration_seconds": step.duration_seconds,
|
384
|
+
"error": step.error,
|
385
|
+
},
|
386
|
+
default=_json_default,
|
387
|
+
)
|
388
|
+
agent_html.append(f'<li><code>{info}</code></li>')
|
389
|
+
|
390
|
+
timeframe = ""
|
391
|
+
if section.get("started_at") or section.get("finished_at"):
|
392
|
+
timeframe = (
|
393
|
+
f"<p class=\"time\">{section.get('started_at', '–')} → "
|
394
|
+
f"{section.get('finished_at', '–')}</p>"
|
395
|
+
)
|
396
|
+
|
397
|
+
agents_block = ""
|
398
|
+
if agent_html:
|
399
|
+
agents_block = '<ul class="agents">' + "".join(agent_html) + "</ul>"
|
400
|
+
|
401
|
+
panels_block = "".join(panels_html)
|
402
|
+
|
403
|
+
block = (
|
404
|
+
f"\n <section class=\"section\">\n"
|
405
|
+
f" <h2>{section['title']}</h2>\n"
|
406
|
+
f" {timeframe}\n"
|
407
|
+
f" {agents_block}\n"
|
408
|
+
f" {panels_block}\n"
|
409
|
+
" </section>\n "
|
410
|
+
).strip()
|
411
|
+
body_sections.append(block)
|
412
|
+
|
413
|
+
sections_html = "\n".join(body_sections) if body_sections else "<p>No panels recorded.</p>"
|
414
|
+
|
415
|
+
return f"""<!DOCTYPE html>
|
416
|
+
<html lang="en">
|
417
|
+
<head>
|
418
|
+
<meta charset="utf-8" />
|
419
|
+
<title>Terminal Log · {self.workflow_name}</title>
|
420
|
+
<style>
|
421
|
+
body {{
|
422
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
423
|
+
margin: 0;
|
424
|
+
padding: 24px;
|
425
|
+
background: #0f172a;
|
426
|
+
color: #e2e8f0;
|
427
|
+
}}
|
428
|
+
h1 {{
|
429
|
+
margin-top: 0;
|
430
|
+
}}
|
431
|
+
.meta {{
|
432
|
+
margin-bottom: 24px;
|
433
|
+
line-height: 1.6;
|
434
|
+
}}
|
435
|
+
.section {{
|
436
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
437
|
+
border-radius: 12px;
|
438
|
+
padding: 16px 20px;
|
439
|
+
margin-bottom: 18px;
|
440
|
+
background: rgba(15, 23, 42, 0.6);
|
441
|
+
}}
|
442
|
+
.section h2 {{
|
443
|
+
margin-top: 0;
|
444
|
+
}}
|
445
|
+
.section .time {{
|
446
|
+
color: #60a5fa;
|
447
|
+
font-size: 0.9rem;
|
448
|
+
margin-top: -8px;
|
449
|
+
}}
|
450
|
+
pre {{
|
451
|
+
background: rgba(15, 23, 42, 0.85);
|
452
|
+
border-radius: 10px;
|
453
|
+
padding: 12px;
|
454
|
+
overflow-x: auto;
|
455
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
456
|
+
white-space: pre-wrap;
|
457
|
+
word-wrap: break-word;
|
458
|
+
}}
|
459
|
+
ul.agents {{
|
460
|
+
list-style: none;
|
461
|
+
padding-left: 0;
|
462
|
+
margin: 0 0 16px 0;
|
463
|
+
}}
|
464
|
+
ul.agents li {{
|
465
|
+
margin-bottom: 6px;
|
466
|
+
}}
|
467
|
+
</style>
|
468
|
+
</head>
|
469
|
+
<body>
|
470
|
+
<header>
|
471
|
+
<h1>Terminal Log · {self.workflow_name}</h1>
|
472
|
+
<div class="meta">
|
473
|
+
<div><strong>Experiment ID:</strong> {self.experiment_id}</div>
|
474
|
+
<div><strong>Started:</strong> {self._started_at_iso or "–"}</div>
|
475
|
+
<div><strong>Finished:</strong> {self._finished_at_iso or "–"}</div>
|
476
|
+
<div><strong>Duration:</strong> {duration} seconds</div>
|
477
|
+
</div>
|
478
|
+
</header>
|
479
|
+
<main>
|
480
|
+
{sections_html}
|
481
|
+
</main>
|
482
|
+
</body>
|
483
|
+
</html>
|
484
|
+
"""
|
485
|
+
|
486
|
+
def _render_final_report(self) -> tuple[str, str]:
|
487
|
+
"""Render final report markdown + HTML."""
|
488
|
+
if isinstance(self._final_result, str):
|
489
|
+
body_md = self._final_result.rstrip()
|
490
|
+
elif self._final_result is not None:
|
491
|
+
body_md = json.dumps(self._final_result, indent=2, default=_json_default)
|
492
|
+
else:
|
493
|
+
body_md = "No final report generated."
|
494
|
+
|
495
|
+
markdown_content = f"# Final Report · {self.workflow_name}\n\n{body_md}\n"
|
496
|
+
|
497
|
+
body_pre = body_md.replace("&", "&").replace("<", "<").replace(">", ">")
|
498
|
+
html_content = f"""<!DOCTYPE html>
|
499
|
+
<html lang="en">
|
500
|
+
<head>
|
501
|
+
<meta charset="utf-8" />
|
502
|
+
<title>Final Report · {self.workflow_name}</title>
|
503
|
+
<style>
|
504
|
+
body {{
|
505
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
506
|
+
margin: 0;
|
507
|
+
padding: 24px;
|
508
|
+
background: #111827;
|
509
|
+
color: #f9fafb;
|
510
|
+
}}
|
511
|
+
h1 {{
|
512
|
+
margin-top: 0;
|
513
|
+
}}
|
514
|
+
pre {{
|
515
|
+
background: #1f2937;
|
516
|
+
border-radius: 10px;
|
517
|
+
padding: 16px;
|
518
|
+
overflow-x: auto;
|
519
|
+
white-space: pre-wrap;
|
520
|
+
word-wrap: break-word;
|
521
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
522
|
+
}}
|
523
|
+
</style>
|
524
|
+
</head>
|
525
|
+
<body>
|
526
|
+
<h1>Final Report · {self.workflow_name}</h1>
|
527
|
+
<pre>{body_pre}</pre>
|
528
|
+
</body>
|
529
|
+
</html>
|
530
|
+
"""
|
531
|
+
return markdown_content, html_content
|
532
|
+
|
533
|
+
# ------------------------------------------------------------------ helpers
|
534
|
+
|
535
|
+
def ensure_started(self) -> None:
|
536
|
+
"""Raise if reporter not initialised."""
|
537
|
+
if self._start_time is None:
|
538
|
+
raise RuntimeError("ArtifactWriter.start must be called before logging events.")
|