hexdag 0.5.0.dev1__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.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,494 @@
1
+ """Pipeline management commands for HexDAG CLI."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ app = typer.Typer()
12
+ console = Console()
13
+
14
+
15
+ @app.command("validate")
16
+ def validate_pipeline(
17
+ pipeline_path: Annotated[
18
+ Path,
19
+ typer.Argument(help="Path to pipeline YAML file"),
20
+ ],
21
+ ) -> None:
22
+ """Validate pipeline file (schema + DAG validation)."""
23
+ import yaml
24
+
25
+ if not pipeline_path.exists():
26
+ console.print(f"[red]Error: Pipeline file not found: {pipeline_path}[/red]")
27
+ raise typer.Exit(1)
28
+
29
+ try:
30
+ # Load YAML
31
+ with open(pipeline_path) as f:
32
+ pipeline_data = yaml.safe_load(f)
33
+
34
+ console.print(f"[cyan]Validating pipeline: {pipeline_path}[/cyan]")
35
+
36
+ issues = []
37
+ warnings = []
38
+
39
+ # Basic structure validation
40
+ if not isinstance(pipeline_data, dict):
41
+ issues.append("Pipeline must be a dictionary")
42
+ console.print("[red]✗ Invalid pipeline structure[/red]")
43
+ raise typer.Exit(1)
44
+
45
+ # Check required fields
46
+ required_fields = ["name", "nodes"]
47
+ issues.extend(
48
+ f"'{field}' is required" for field in required_fields if field not in pipeline_data
49
+ )
50
+
51
+ # Validate nodes section
52
+ if "nodes" in pipeline_data and not issues:
53
+ nodes = pipeline_data["nodes"]
54
+ if not isinstance(nodes, list):
55
+ issues.append("'nodes' must be a list")
56
+ else:
57
+ console.print(f"[dim]Found {len(nodes)} node(s)[/dim]")
58
+
59
+ # Validate each node
60
+ node_ids = set()
61
+ for i, node in enumerate(nodes):
62
+ if not isinstance(node, dict):
63
+ issues.append(f"Node {i} must be a dictionary")
64
+ continue
65
+
66
+ # Check node ID
67
+ if "id" not in node:
68
+ issues.append(f"Node {i} missing 'id' field")
69
+ else:
70
+ node_id = node["id"]
71
+ if node_id in node_ids:
72
+ issues.append(f"Duplicate node ID: '{node_id}'")
73
+ node_ids.add(node_id)
74
+
75
+ console.print(
76
+ f" [green]✓[/green] {node_id} ({node.get('type', 'unknown')})"
77
+ )
78
+
79
+ # Check node type
80
+ if "type" not in node:
81
+ warnings.append(f"Node {node.get('id', i)} missing 'type' field")
82
+
83
+ # Validate depends_on references
84
+ if "depends_on" in node:
85
+ depends_on = node["depends_on"]
86
+ if not isinstance(depends_on, list):
87
+ issues.append(f"Node {node.get('id', i)}: 'depends_on' must be a list")
88
+ # Defer dependency validation until all node IDs are collected
89
+
90
+ # Validate dependency references (after all node IDs collected)
91
+ for i, node in enumerate(nodes):
92
+ if isinstance(node, dict) and "depends_on" in node:
93
+ depends_on = node["depends_on"]
94
+ if isinstance(depends_on, list):
95
+ issues.extend(
96
+ f"Node '{node.get('id', i)}' dependency '{dep}' not found"
97
+ for dep in depends_on
98
+ if dep not in node_ids
99
+ )
100
+
101
+ # DAG validation - check for cycles
102
+ if not issues:
103
+ console.print("\n[cyan]Checking DAG for cycles...[/cyan]")
104
+ if cycles := _detect_cycles(nodes):
105
+ issues.append(f"DAG contains cycle(s): {cycles}")
106
+ console.print(f"[red]✗ Cycle detected: {' -> '.join(cycles)}[/red]")
107
+ else:
108
+ console.print("[green]✓ No cycles detected[/green]")
109
+
110
+ # Report results
111
+ if issues:
112
+ console.print("\n[red]Validation errors:[/red]")
113
+ for issue in issues:
114
+ console.print(f" [red]✗[/red] {issue}")
115
+ raise typer.Exit(1)
116
+
117
+ if warnings:
118
+ console.print("\n[yellow]Warnings:[/yellow]")
119
+ for warning in warnings:
120
+ console.print(f" [yellow]⚠[/yellow] {warning}")
121
+
122
+ console.print("\n[green]✓ Pipeline validation passed[/green]")
123
+ console.print(f" Name: {pipeline_data.get('name', 'unnamed')}")
124
+ console.print(f" Nodes: {len(pipeline_data.get('nodes', []))}")
125
+
126
+ except yaml.YAMLError as e:
127
+ console.print(f"[red]YAML parsing error: {e}[/red]")
128
+ raise typer.Exit(1)
129
+ except Exception as e:
130
+ console.print(f"[red]Validation error: {e}[/red]")
131
+ raise typer.Exit(1)
132
+
133
+
134
+ @app.command("graph")
135
+ def generate_graph(
136
+ pipeline_path: Annotated[
137
+ Path,
138
+ typer.Argument(help="Path to pipeline YAML file"),
139
+ ],
140
+ output: Annotated[
141
+ Path | None,
142
+ typer.Option(
143
+ "--out",
144
+ "-o",
145
+ help="Output file path (supports .svg, .png, .dot)",
146
+ ),
147
+ ] = None,
148
+ ) -> None:
149
+ """Generate visual graph of pipeline DAG."""
150
+ import yaml
151
+
152
+ if not pipeline_path.exists():
153
+ console.print(f"[red]Error: Pipeline file not found: {pipeline_path}[/red]")
154
+ raise typer.Exit(1)
155
+
156
+ try:
157
+ try:
158
+ import graphviz
159
+ except ImportError:
160
+ console.print("[red]Error: graphviz not installed[/red]")
161
+ console.print("Install with: uv pip install graphviz")
162
+ raise typer.Exit(1)
163
+
164
+ # Load pipeline
165
+ with open(pipeline_path) as f:
166
+ pipeline_data = yaml.safe_load(f)
167
+
168
+ console.print(f"[cyan]Generating graph for: {pipeline_path}[/cyan]")
169
+
170
+ pipeline_name = pipeline_data.get("name", "pipeline")
171
+ dot = graphviz.Digraph(
172
+ name=pipeline_name,
173
+ comment=f"Pipeline: {pipeline_name}",
174
+ format="svg" if not output else output.suffix[1:],
175
+ )
176
+
177
+ # Configure graph appearance
178
+ dot.attr(rankdir="TB") # Top to bottom
179
+ dot.attr("node", shape="box", style="rounded,filled", fillcolor="lightblue")
180
+
181
+ nodes = pipeline_data.get("nodes", [])
182
+
183
+ for node in nodes:
184
+ node_id = node.get("id", "unknown")
185
+ node_type = node.get("type", "unknown")
186
+ label = f"{node_id}\\n({node_type})"
187
+
188
+ # Color by type
189
+ color = "lightblue"
190
+ if node_type in ["agent", "agent_node"]:
191
+ color = "lightgreen"
192
+ elif node_type in ["llm", "llm_node"]:
193
+ color = "lightyellow"
194
+ elif node_type in ["function", "function_node"]:
195
+ color = "lightgray"
196
+ elif node_type in ["conditional", "conditional_node"]:
197
+ color = "orange"
198
+
199
+ dot.node(node_id, label=label, fillcolor=color)
200
+
201
+ for node in nodes:
202
+ node_id = node.get("id")
203
+ depends_on = node.get("depends_on", [])
204
+ for dep in depends_on:
205
+ dot.edge(dep, node_id)
206
+
207
+ # Determine output path
208
+ output_path = output or pipeline_path.with_suffix(".svg")
209
+
210
+ # Render graph
211
+ output_base = str(output_path.with_suffix(""))
212
+ dot.render(output_base, cleanup=True)
213
+
214
+ console.print(f"[green]✓ Graph generated: {output_path}[/green]")
215
+ console.print(f" Nodes: {len(nodes)}")
216
+ console.print(f" Edges: {sum(len(n.get('depends_on', [])) for n in nodes)}")
217
+
218
+ except Exception as e:
219
+ console.print(f"[red]Error generating graph: {e}[/red]")
220
+ raise typer.Exit(1)
221
+
222
+
223
+ @app.command("plan")
224
+ def plan_execution(
225
+ pipeline_path: Annotated[
226
+ Path,
227
+ typer.Argument(help="Path to pipeline YAML file"),
228
+ ],
229
+ ) -> None:
230
+ """Show execution plan (waves, concurrency, expected I/O)."""
231
+ import yaml
232
+
233
+ if not pipeline_path.exists():
234
+ console.print(f"[red]Error: Pipeline file not found: {pipeline_path}[/red]")
235
+ raise typer.Exit(1)
236
+
237
+ try:
238
+ # Load pipeline
239
+ with open(pipeline_path) as f:
240
+ pipeline_data = yaml.safe_load(f)
241
+
242
+ console.print(f"[cyan]Execution plan for: {pipeline_data.get('name', 'unnamed')}[/cyan]\n")
243
+
244
+ nodes = pipeline_data.get("nodes", [])
245
+
246
+ # Calculate execution waves (topological sort)
247
+ waves = _calculate_waves(nodes)
248
+
249
+ # Display waves
250
+ console.print("[bold]Execution Waves:[/bold]")
251
+ for wave_num, wave_nodes in enumerate(waves, 1):
252
+ console.print(f"\n[yellow]Wave {wave_num}:[/yellow] (parallel execution)")
253
+ for node_id in wave_nodes:
254
+ # Find node details
255
+ node: dict = next((n for n in nodes if n.get("id") == node_id), {})
256
+ node_type = node.get("type", "unknown")
257
+ console.print(f" • {node_id} [{node_type}]")
258
+
259
+ # Summary
260
+ console.print("\n[bold]Summary:[/bold]")
261
+ console.print(f" Total nodes: {len(nodes)}")
262
+ console.print(f" Execution waves: {len(waves)}")
263
+ console.print(f" Max concurrency: {max(len(wave) for wave in waves) if waves else 0}")
264
+ console.print(f" Estimated steps: {len(waves)}")
265
+
266
+ # Calculate I/O expectations
267
+ io_nodes = [n for n in nodes if n.get("type") in ["agent", "agent_node", "llm", "llm_node"]]
268
+ console.print(f" Expected LLM calls: {len(io_nodes)}")
269
+
270
+ except Exception as e:
271
+ console.print(f"[red]Error generating plan: {e}[/red]")
272
+ raise typer.Exit(1)
273
+
274
+
275
+ def _detect_cycles(nodes: list) -> list[str] | None:
276
+ """Detect cycles in DAG using DFS."""
277
+ graph = {}
278
+ for node in nodes:
279
+ node_id = node.get("id")
280
+ graph[node_id] = node.get("depends_on", [])
281
+
282
+ # DFS to detect cycles
283
+ visited = set()
284
+ rec_stack = set()
285
+ path = []
286
+
287
+ def dfs(node_id: str) -> list[str] | None:
288
+ visited.add(node_id)
289
+ rec_stack.add(node_id)
290
+ path.append(node_id)
291
+
292
+ for neighbor in graph.get(node_id, []):
293
+ if neighbor not in visited:
294
+ result = dfs(neighbor)
295
+ if result is not None:
296
+ return result
297
+ elif neighbor in rec_stack:
298
+ # Found cycle
299
+ cycle_start = path.index(neighbor)
300
+ return path[cycle_start:] + [neighbor]
301
+
302
+ path.pop()
303
+ rec_stack.remove(node_id)
304
+ return None
305
+
306
+ for node_id in graph:
307
+ if node_id not in visited:
308
+ result = dfs(node_id)
309
+ if result is not None:
310
+ return result
311
+
312
+ return None
313
+
314
+
315
+ def _calculate_waves(nodes: list) -> list[list[str]]:
316
+ """Calculate execution waves using topological sort."""
317
+ graph = {}
318
+ in_degree = {}
319
+
320
+ for node in nodes:
321
+ node_id = node.get("id")
322
+ graph[node_id] = node.get("depends_on", [])
323
+ in_degree[node_id] = 0
324
+
325
+ # Calculate in-degrees
326
+ for node_id, deps in graph.items():
327
+ for dep in deps:
328
+ if dep in in_degree:
329
+ in_degree[node_id] += 1
330
+
331
+ waves = []
332
+ remaining = set(graph.keys())
333
+
334
+ while remaining:
335
+ # Find nodes with no dependencies
336
+ wave = [node_id for node_id in remaining if in_degree[node_id] == 0]
337
+
338
+ if not wave:
339
+ # Shouldn't happen if DAG is valid
340
+ break
341
+
342
+ waves.append(wave)
343
+
344
+ # Remove processed nodes and update in-degrees
345
+ for node_id in wave:
346
+ remaining.remove(node_id)
347
+ for other_id in remaining:
348
+ if node_id in graph[other_id]:
349
+ in_degree[other_id] -= 1
350
+
351
+ return waves
352
+
353
+
354
+ @app.command("run")
355
+ def run_pipeline(
356
+ pipeline_path: Annotated[
357
+ Path,
358
+ typer.Argument(help="Path to pipeline YAML file"),
359
+ ],
360
+ input_data: Annotated[
361
+ str | None,
362
+ typer.Option(
363
+ "--input",
364
+ "-i",
365
+ help='Input data as JSON string (e.g., \'{"key": "value"}\')',
366
+ ),
367
+ ] = None,
368
+ input_file: Annotated[
369
+ Path | None,
370
+ typer.Option(
371
+ "--input-file",
372
+ "-f",
373
+ help="Input data from JSON file",
374
+ ),
375
+ ] = None,
376
+ output_file: Annotated[
377
+ Path | None,
378
+ typer.Option(
379
+ "--output",
380
+ "-o",
381
+ help="Save output to JSON file",
382
+ ),
383
+ ] = None,
384
+ verbose: Annotated[
385
+ bool,
386
+ typer.Option(
387
+ "--verbose",
388
+ "-v",
389
+ help="Show detailed execution information",
390
+ ),
391
+ ] = False,
392
+ ) -> None:
393
+ """Execute a pipeline with optional input data."""
394
+ import asyncio
395
+
396
+ if not pipeline_path.exists():
397
+ console.print(f"[red]Error: Pipeline file not found: {pipeline_path}[/red]")
398
+ raise typer.Exit(1)
399
+
400
+ try:
401
+ # Parse input data
402
+ inputs = {}
403
+ if input_data:
404
+ try:
405
+ inputs = json.loads(input_data)
406
+ except json.JSONDecodeError as e:
407
+ console.print(f"[red]Error: Invalid JSON in --input: {e}[/red]")
408
+ raise typer.Exit(1)
409
+ elif input_file:
410
+ if not input_file.exists():
411
+ console.print(f"[red]Error: Input file not found: {input_file}[/red]")
412
+ raise typer.Exit(1)
413
+ try:
414
+ with open(input_file) as f:
415
+ inputs = json.load(f)
416
+ except json.JSONDecodeError as e:
417
+ console.print(f"[red]Error: Invalid JSON in input file: {e}[/red]")
418
+ raise typer.Exit(1)
419
+
420
+ # Import hexdag components
421
+ from hexdag import Orchestrator, YamlPipelineBuilder
422
+
423
+ if verbose:
424
+ console.print(f"[cyan]Loading pipeline: {pipeline_path}[/cyan]")
425
+
426
+ builder = YamlPipelineBuilder()
427
+ graph, pipeline_config = builder.build_from_yaml_file(str(pipeline_path))
428
+
429
+ if verbose:
430
+ console.print(f"[dim]Pipeline: {pipeline_config.metadata.get('name', 'unnamed')}[/dim]")
431
+ console.print(f"[dim]Nodes: {len(graph.nodes)}[/dim]\n")
432
+
433
+ # Execute pipeline
434
+ console.print("[cyan]Executing pipeline...[/cyan]")
435
+
436
+ orchestrator = Orchestrator()
437
+ result = asyncio.run(orchestrator.run(graph, inputs))
438
+
439
+ # Display results
440
+ if verbose:
441
+ console.print("\n[green]✓ Pipeline execution completed[/green]\n")
442
+
443
+ # Show results in a table
444
+ table = Table(title="Pipeline Results", show_header=True, header_style="bold magenta")
445
+ table.add_column("Node", style="cyan")
446
+ table.add_column("Status", style="green")
447
+ table.add_column("Output", style="dim")
448
+
449
+ for node_id, node_result in result.items():
450
+ status = "✓" if node_result else "✗"
451
+ output_preview = (
452
+ str(node_result)[:50] + "..."
453
+ if len(str(node_result)) > 50
454
+ else str(node_result)
455
+ )
456
+ table.add_row(node_id, status, output_preview)
457
+
458
+ console.print(table)
459
+ else:
460
+ console.print("[green]✓ Pipeline execution completed[/green]")
461
+
462
+ # Save output if requested
463
+ if output_file:
464
+ output_data = {}
465
+ for k, v in result.items():
466
+ try:
467
+ # Try to serialize directly
468
+ json.dumps({k: v})
469
+ output_data[k] = v
470
+ except (TypeError, ValueError):
471
+ # Fall back to string representation
472
+ output_data[k] = str(v)
473
+
474
+ with open(output_file, "w") as f:
475
+ json.dump(output_data, f, indent=2)
476
+ console.print(f"[dim]Output saved to: {output_file}[/dim]")
477
+
478
+ # Print final result
479
+ if not verbose and not output_file:
480
+ console.print("\n[bold]Results:[/bold]")
481
+ for node_id, node_result in result.items():
482
+ console.print(f" {node_id}: {node_result}")
483
+
484
+ except ImportError as e:
485
+ console.print(f"[red]Error: Missing dependency - {e}[/red]")
486
+ console.print("Install with: uv pip install hexdag[all]")
487
+ raise typer.Exit(1)
488
+ except Exception as e:
489
+ console.print(f"[red]Error executing pipeline: {e}[/red]")
490
+ if verbose:
491
+ import traceback
492
+
493
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
494
+ raise typer.Exit(1)