|
1 | | -"""Export forge results to a project directory.""" |
2 | | - |
3 | | -from __future__ import annotations |
| 1 | +"""SOTA Artifact Exporter (I/O). |
4 | 2 |
|
| 3 | +Transforms internal representations into Enterprise-Ready Deliverables: |
| 4 | +- 📊 Mermaid.js embedded Markdown for visual Claim Lattices. |
| 5 | +- 📓 Jupyter Notebooks (.ipynb) for Kaggle Baselines. |
| 6 | +- 🧠Obsidian-compatible JSON graphs for Personal Knowledge Management (PKM). |
| 7 | +""" |
| 8 | +import os |
5 | 9 | import json |
6 | 10 | from pathlib import Path |
7 | | -from typing import Union |
8 | | - |
| 11 | +from loguru import logger |
| 12 | +import nbformat as nbf |
9 | 13 | from epistemic_forge.models import ForgeResult |
10 | 14 |
|
| 15 | +def _generate_mermaid_graph(claims) -> str: |
| 16 | + """Generates a Mermaid.js flowchart from the Claim Lattice.""" |
| 17 | + lines = ["graph TD"] |
| 18 | + for c in claims: |
| 19 | + # Pydantic safety |
| 20 | + c_dict = c.model_dump() if hasattr(c, "model_dump") else c |
| 21 | + c_id = c_dict.get("id", "Unknown") |
| 22 | + c_text = c_dict.get("text", "").replace('"', "'")[:50] + "..." |
| 23 | + lines.append(f' {c_id}["{c_id}: {c_text}"]') |
| 24 | + |
| 25 | + for s in c_dict.get("support", []): |
| 26 | + lines.append(f' {c_id} -->|Supports| S_{hash(s) % 1000}["{s[:40]}..."]') |
| 27 | + for o in c_dict.get("objections", []): |
| 28 | + lines.append(f' {c_id} -.->|Objects| O_{hash(o) % 1000}["{o[:40]}..."]') |
| 29 | + return "\n".join(lines) |
| 30 | + |
| 31 | +def _export_jupyter_notebook(artifact, out_path: Path): |
| 32 | + """Converts a python/markdown artifact into a runnable Jupyter Notebook.""" |
| 33 | + nb = nbf.v4.new_notebook() |
| 34 | + cells = [] |
| 35 | + |
| 36 | + # Split artifact content heuristically (Markdown vs Code) |
| 37 | + chunks = artifact.content.split("```python") |
| 38 | + cells.append(nbf.v4.new_markdown_cell(chunks[0])) |
| 39 | + |
| 40 | + for chunk in chunks[1:]: |
| 41 | + if "```" in chunk: |
| 42 | + code, md = chunk.split("```", 1) |
| 43 | + cells.append(nbf.v4.new_code_cell(code.strip())) |
| 44 | + if md.strip(): |
| 45 | + cells.append(nbf.v4.new_markdown_cell(md.strip())) |
| 46 | + else: |
| 47 | + cells.append(nbf.v4.new_code_cell(chunk.strip())) |
| 48 | + |
| 49 | + nb.cells = cells |
| 50 | + with open(out_path, "w", encoding="utf-8") as f: |
| 51 | + nbf.write(nb, f) |
11 | 52 |
|
12 | | -def export_result(result: ForgeResult, out_dir: Union[str, Path]) -> Path: |
| 53 | +def export_result(result: ForgeResult, out_dir: str): |
| 54 | + """Main SOTA Export router.""" |
13 | 55 | out = Path(out_dir) |
14 | 56 | out.mkdir(parents=True, exist_ok=True) |
15 | | - result_dict = ( |
16 | | - result.model_dump() if hasattr(result, "model_dump") else result.to_dict() |
17 | | - ) |
18 | | - (out / "result.json").write_text( |
19 | | - json.dumps(result_dict, indent=2, ensure_ascii=False), encoding="utf-8" |
20 | | - ) |
| 57 | + logger.info(f"💾 SOTA I/O: Exporting Enterprise Artifacts to {out.resolve()}...") |
| 58 | + |
| 59 | + # 1. Executive Summary & Memo (with Mermaid Graph) |
| 60 | + memo_path = out / "executive_summary.md" |
| 61 | + mermaid_code = _generate_mermaid_graph(result.claims) |
| 62 | + |
| 63 | + memo_content = f"""--- |
| 64 | +title: {result.spec.title} |
| 65 | +domain: {result.spec.domain} |
| 66 | +score: {result.final_score} |
| 67 | +--- |
| 68 | +# Executive Synthesis |
| 69 | +**Inquiry:** {result.spec.question} |
| 70 | +
|
| 71 | +## Epistemic Claim Lattice (Visual) |
| 72 | +```mermaid |
| 73 | +{mermaid_code} |
| 74 | +``` |
| 75 | +
|
| 76 | +## AI Peer Review Verdict |
| 77 | +**Verdict:** `{result.peer_review.get('verdict', 'Unknown').upper()}` |
| 78 | +**Critique:** {result.peer_review.get('final_comments', '')} |
| 79 | +""" |
| 80 | + |
| 81 | + # Add final synthesized text |
| 82 | + for art in result.artifacts: |
| 83 | + if art.name == "Final Synthesis Memo": |
| 84 | + memo_content += f"\n\n## Deep Synthesis\n\n{art.content}" |
| 85 | + |
| 86 | + with open(memo_path, "w", encoding="utf-8") as f: |
| 87 | + f.write(memo_content) |
| 88 | + |
| 89 | + # 2. Machine-Readable Knowledge Graph (Obsidian/JSON) |
| 90 | + json_path = out / "claim_lattice_graph.json" |
| 91 | + with open(json_path, "w", encoding="utf-8") as f: |
| 92 | + # Convert claims to a node-edge graph format |
| 93 | + nodes = [] |
| 94 | + edges = [] |
| 95 | + for c in result.claims: |
| 96 | + c_dict = c.model_dump() if hasattr(c, "model_dump") else c |
| 97 | + nodes.append({"id": c_dict.get("id"), "label": c_dict.get("text"), "warrant": c_dict.get("epistemic_warrant")}) |
| 98 | + |
| 99 | + graph_data = {"nodes": nodes, "edges": edges, "metadata": result.peer_review} |
| 100 | + json.dump(graph_data, f, indent=2) |
| 101 | + |
| 102 | + # 3. Dynamic Artifacts (Jupyter Notebooks for Kaggle/Code) |
21 | 103 | for art in result.artifacts: |
22 | | - # path_hint like outputs/foo.md → use name + suffix |
23 | | - name = art.path_hint.split("/")[-1] if art.path_hint else f"{art.name}.txt" |
24 | | - (out / name).write_text(art.content, encoding="utf-8") |
25 | | - # Manifest |
26 | | - manifest = { |
27 | | - "title": result.spec.title, |
28 | | - "domain": result.spec.domain.value, |
29 | | - "score": result.final_score, |
30 | | - "review": result.peer_review, |
31 | | - "files": [a.path_hint or a.name for a in result.artifacts], |
32 | | - "route": result.route.model_dump() |
33 | | - if hasattr(result.route, "model_dump") |
34 | | - else result.route.to_dict(), |
35 | | - "instruction": result.instruction, |
36 | | - } |
37 | | - (out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8") |
38 | | - return out |
| 104 | + if art.kind in ["python", "notebook", "kaggle"]: |
| 105 | + nb_path = out / (art.path_hint or "baseline.ipynb") |
| 106 | + if not str(nb_path).endswith(".ipynb"): |
| 107 | + nb_path = nb_path.with_suffix(".ipynb") |
| 108 | + _export_jupyter_notebook(art, nb_path) |
| 109 | + |
| 110 | + logger.success(f"💾 Export Complete. Files ready in {out.resolve()}") |
0 commit comments