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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- 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)
|