devpilot-agentic-cli 1.0.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.
agent/tools/diagram.py ADDED
@@ -0,0 +1,131 @@
1
+ """
2
+ agent/tools/diagram.py
3
+ ──────────────────────
4
+ Mermaid diagram generation tool.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ import tempfile
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from agent.tools.base import BaseTool, ToolResult, ToolSchema
15
+ from agent.tools.fs import _safe_path
16
+
17
+ if TYPE_CHECKING:
18
+ from agent.config import Config
19
+
20
+
21
+ _MERMAID_TEMPLATE = """flowchart TD
22
+ A[Start] --> B[Process]
23
+ B --> C[End]
24
+ """
25
+
26
+
27
+ class DiagramTool(BaseTool):
28
+ """Generate Mermaid diagrams and save them as SVG or PNG files."""
29
+
30
+ def __init__(self, config: "Config") -> None:
31
+ self._config = config
32
+
33
+ @property
34
+ def schema(self) -> ToolSchema:
35
+ return ToolSchema(
36
+ name="generate_diagram",
37
+ description=(
38
+ "Generate a diagram from Mermaid syntax. "
39
+ "Saves as SVG (default) or PNG using mermaid-cli (mmdc). "
40
+ "Also writes the source .mmd file. "
41
+ "Use for flowcharts, sequence diagrams, ER diagrams, class diagrams, etc. "
42
+ "Mermaid docs: https://mermaid.js.org/"
43
+ ),
44
+ parameters={
45
+ "type": "object",
46
+ "properties": {
47
+ "mermaid_code": {
48
+ "type": "string",
49
+ "description": "Valid Mermaid diagram definition.",
50
+ },
51
+ "output_path": {
52
+ "type": "string",
53
+ "description": "Output file path without extension (e.g. 'docs/architecture').",
54
+ },
55
+ "format": {
56
+ "type": "string",
57
+ "enum": ["svg", "png", "mmd"],
58
+ "description": "Output format. 'mmd' saves source only. Default: svg.",
59
+ "default": "svg",
60
+ },
61
+ "theme": {
62
+ "type": "string",
63
+ "enum": ["default", "dark", "neutral", "forest"],
64
+ "description": "Mermaid theme. Default: default.",
65
+ "default": "default",
66
+ },
67
+ },
68
+ "required": ["mermaid_code", "output_path"],
69
+ },
70
+ required=["mermaid_code", "output_path"],
71
+ is_destructive=True,
72
+ sprint="Sprint 2",
73
+ )
74
+
75
+ async def execute( # type: ignore[override]
76
+ self,
77
+ mermaid_code: str,
78
+ output_path: str,
79
+ format: str = "svg",
80
+ theme: str = "default",
81
+ ) -> ToolResult:
82
+ results: list[str] = []
83
+
84
+ # Always write the .mmd source file
85
+ mmd_path = _safe_path(self._config.workdir, output_path + ".mmd")
86
+ try:
87
+ mmd_path.parent.mkdir(parents=True, exist_ok=True)
88
+ mmd_path.write_text(mermaid_code, encoding="utf-8")
89
+ results.append(f"✓ Mermaid source written to {output_path}.mmd")
90
+ except Exception as e:
91
+ return ToolResult(f"Error writing .mmd file: {e}", is_error=True)
92
+
93
+ if format == "mmd":
94
+ return ToolResult("\n".join(results), is_error=False)
95
+
96
+ # Try to render via mmdc (mermaid-cli)
97
+ output_file = _safe_path(self._config.workdir, f"{output_path}.{format}")
98
+ try:
99
+ proc = subprocess.run(
100
+ [
101
+ "mmdc",
102
+ "-i", str(mmd_path),
103
+ "-o", str(output_file),
104
+ "-t", theme,
105
+ "--backgroundColor", "transparent",
106
+ ],
107
+ capture_output=True,
108
+ text=True,
109
+ timeout=30,
110
+ )
111
+ if proc.returncode == 0:
112
+ results.append(f"✓ Diagram rendered to {output_path}.{format}")
113
+ else:
114
+ err = proc.stderr.strip() or proc.stdout.strip()
115
+ results.append(
116
+ f"⚠ mmdc rendering failed: {err}\n"
117
+ f" The .mmd source file was saved — render it manually with:\n"
118
+ f" npx @mermaid-js/mermaid-cli -i {output_path}.mmd -o {output_path}.{format}"
119
+ )
120
+ except FileNotFoundError:
121
+ results.append(
122
+ f"⚠ mmdc (mermaid-cli) not found. The .mmd source was saved.\n"
123
+ f" Install with: npm install -g @mermaid-js/mermaid-cli\n"
124
+ f" Then render: mmdc -i {output_path}.mmd -o {output_path}.{format}"
125
+ )
126
+ except subprocess.TimeoutExpired:
127
+ results.append(f"⚠ mmdc timed out after 30s. The .mmd source was saved.")
128
+ except Exception as e:
129
+ results.append(f"⚠ Rendering error: {e}. The .mmd source was saved.")
130
+
131
+ return ToolResult("\n".join(results), is_error=False)
agent/tools/doc_gen.py ADDED
@@ -0,0 +1,163 @@
1
+ """
2
+ agent/tools/doc_gen.py
3
+ ──────────────────────
4
+ Documentation generation tool — Markdown + PDF output.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from agent.tools.base import BaseTool, ToolResult, ToolSchema
13
+ from agent.tools.fs import _safe_path
14
+
15
+ if TYPE_CHECKING:
16
+ from agent.config import Config
17
+
18
+
19
+ class DocGenTool(BaseTool):
20
+ """Generate Markdown and PDF documentation from provided content."""
21
+
22
+ def __init__(self, config: "Config") -> None:
23
+ self._config = config
24
+
25
+ @property
26
+ def schema(self) -> ToolSchema:
27
+ return ToolSchema(
28
+ name="generate_docs",
29
+ description=(
30
+ "Generate documentation files. Converts Markdown to a polished PDF "
31
+ "or writes a Markdown file. Use to create README files, "
32
+ "API docs, reports, or any structured text document."
33
+ ),
34
+ parameters={
35
+ "type": "object",
36
+ "properties": {
37
+ "content": {
38
+ "type": "string",
39
+ "description": "Markdown content to write.",
40
+ },
41
+ "output_path": {
42
+ "type": "string",
43
+ "description": "Output file path (e.g. 'docs/README.md' or 'docs/report.pdf').",
44
+ },
45
+ "format": {
46
+ "type": "string",
47
+ "enum": ["markdown", "pdf", "both"],
48
+ "description": "Output format: 'markdown', 'pdf', or 'both'. Default: 'markdown'.",
49
+ "default": "markdown",
50
+ },
51
+ "title": {
52
+ "type": "string",
53
+ "description": "Document title for PDF header.",
54
+ },
55
+ },
56
+ "required": ["content", "output_path"],
57
+ },
58
+ required=["content", "output_path"],
59
+ is_destructive=True,
60
+ sprint="Sprint 2",
61
+ )
62
+
63
+ async def execute( # type: ignore[override]
64
+ self,
65
+ content: str,
66
+ output_path: str,
67
+ format: str = "markdown",
68
+ title: str | None = None,
69
+ ) -> ToolResult:
70
+ results: list[str] = []
71
+
72
+ # Write Markdown
73
+ if format in ("markdown", "both"):
74
+ md_path = output_path if output_path.endswith(".md") else output_path.rstrip(".pdf") + ".md"
75
+ try:
76
+ safe_p = _safe_path(self._config.workdir, md_path)
77
+ safe_p.parent.mkdir(parents=True, exist_ok=True)
78
+ safe_p.write_text(content, encoding="utf-8")
79
+ results.append(f"✓ Markdown written to {md_path}")
80
+ except Exception as e:
81
+ return ToolResult(f"Error writing Markdown: {e}", is_error=True)
82
+
83
+ # Write PDF
84
+ if format in ("pdf", "both"):
85
+ pdf_path = output_path if output_path.endswith(".pdf") else output_path.rstrip(".md") + ".pdf"
86
+ try:
87
+ html_content = _markdown_to_html(content, title=title)
88
+ safe_pdf = _safe_path(self._config.workdir, pdf_path)
89
+ safe_pdf.parent.mkdir(parents=True, exist_ok=True)
90
+ _html_to_pdf(html_content, str(safe_pdf))
91
+ results.append(f"✓ PDF written to {pdf_path}")
92
+ except PdfGenerationError as e:
93
+ results.append(f"⚠ PDF generation failed: {e}")
94
+ except Exception as e:
95
+ results.append(f"⚠ PDF error: {e}")
96
+
97
+ if not results:
98
+ return ToolResult("Error: No output was generated.", is_error=True)
99
+
100
+ return ToolResult("\n".join(results), is_error=False)
101
+
102
+
103
+ class PdfGenerationError(Exception):
104
+ pass
105
+
106
+
107
+ def _markdown_to_html(md_content: str, title: str | None = None) -> str:
108
+ """Convert markdown to an HTML string with basic styling."""
109
+ try:
110
+ from markdown_it import MarkdownIt # type: ignore[import]
111
+ md = MarkdownIt()
112
+ body = md.render(md_content)
113
+ except ImportError:
114
+ # Minimal fallback: wrap in <pre>
115
+ body = f"<pre>{md_content}</pre>"
116
+
117
+ doc_title = title or "Document"
118
+ return f"""<!DOCTYPE html>
119
+ <html>
120
+ <head>
121
+ <meta charset="UTF-8">
122
+ <title>{doc_title}</title>
123
+ <style>
124
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
125
+ max-width: 860px; margin: 40px auto; line-height: 1.6; color: #1a1a1a; }}
126
+ h1, h2, h3 {{ border-bottom: 1px solid #e0e0e0; padding-bottom: 4px; }}
127
+ code {{ background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-size: 0.9em; }}
128
+ pre code {{ display: block; padding: 12px; overflow-x: auto; }}
129
+ blockquote {{ border-left: 4px solid #ccc; margin: 0; padding: 8px 16px; color: #555; }}
130
+ table {{ border-collapse: collapse; width: 100%; }}
131
+ th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
132
+ th {{ background: #f0f0f0; }}
133
+ </style>
134
+ </head>
135
+ <body>
136
+ {f'<h1>{doc_title}</h1>' if title else ''}
137
+ {body}
138
+ </body>
139
+ </html>"""
140
+
141
+
142
+ def _html_to_pdf(html_content: str, output_path: str) -> None:
143
+ """Convert HTML string to PDF file using pdfkit (requires wkhtmltopdf)."""
144
+ try:
145
+ import pdfkit # type: ignore[import]
146
+ options = {
147
+ "page-size": "A4",
148
+ "margin-top": "20mm",
149
+ "margin-bottom": "20mm",
150
+ "margin-left": "20mm",
151
+ "margin-right": "20mm",
152
+ "encoding": "UTF-8",
153
+ "quiet": "",
154
+ }
155
+ pdfkit.from_string(html_content, output_path, options=options)
156
+ except ImportError:
157
+ raise PdfGenerationError("pdfkit not installed. Run: pip install pdfkit")
158
+ except OSError as e:
159
+ if "wkhtmltopdf" in str(e).lower():
160
+ raise PdfGenerationError(
161
+ "wkhtmltopdf binary not found. Download from: https://wkhtmltopdf.org/downloads.html"
162
+ )
163
+ raise PdfGenerationError(str(e))