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,558 @@
|
|
|
1
|
+
"""Export API for hexdag studio.
|
|
2
|
+
|
|
3
|
+
Generates complete standalone Python projects from pipelines.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from yaml import safe_load
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/export", tags=["export"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExportRequest(BaseModel):
|
|
18
|
+
"""Request to export a pipeline as a standalone project."""
|
|
19
|
+
|
|
20
|
+
content: str
|
|
21
|
+
project_name: str | None = None
|
|
22
|
+
include_docker: bool = False
|
|
23
|
+
python_version: str = "3.12"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExportedFile(BaseModel):
|
|
27
|
+
"""A single exported file."""
|
|
28
|
+
|
|
29
|
+
path: str
|
|
30
|
+
content: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ExportResponse(BaseModel):
|
|
34
|
+
"""Complete exported project."""
|
|
35
|
+
|
|
36
|
+
success: bool
|
|
37
|
+
project_name: str
|
|
38
|
+
files: list[ExportedFile]
|
|
39
|
+
error: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def slugify(name: str) -> str:
|
|
43
|
+
"""Convert name to valid Python package name."""
|
|
44
|
+
slug = re.sub(r"[^a-zA-Z0-9_]", "_", name.lower())
|
|
45
|
+
slug = re.sub(r"_+", "_", slug)
|
|
46
|
+
return slug.strip("_") or "pipeline"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_env_vars(content: str) -> list[str]:
|
|
50
|
+
"""Extract environment variable references from YAML."""
|
|
51
|
+
# Match ${VAR_NAME} patterns
|
|
52
|
+
pattern = r"\$\{([A-Z_][A-Z0-9_]*)\}"
|
|
53
|
+
return list(set(re.findall(pattern, content)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_adapters(pipeline: dict[str, Any]) -> dict[str, list[str]]:
|
|
57
|
+
"""Detect which adapters are used in the pipeline."""
|
|
58
|
+
adapters: dict[str, list[str]] = {
|
|
59
|
+
"llm": [],
|
|
60
|
+
"memory": [],
|
|
61
|
+
"database": [],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ports = pipeline.get("spec", {}).get("ports", {})
|
|
65
|
+
for port_type, config in ports.items():
|
|
66
|
+
if isinstance(config, dict):
|
|
67
|
+
adapter_name = config.get("adapter", "")
|
|
68
|
+
if port_type in adapters:
|
|
69
|
+
adapters[port_type].append(adapter_name)
|
|
70
|
+
|
|
71
|
+
# Also check nodes for implicit adapter usage
|
|
72
|
+
nodes = pipeline.get("spec", {}).get("nodes", [])
|
|
73
|
+
for node in nodes:
|
|
74
|
+
kind = node.get("kind", "")
|
|
75
|
+
if kind in ("llm_node", "raw_llm_node", "agent_node") and not adapters["llm"]:
|
|
76
|
+
adapters["llm"].append("openai") # Default
|
|
77
|
+
if kind == "agent_node" and not adapters["memory"]:
|
|
78
|
+
adapters["memory"].append("in_memory")
|
|
79
|
+
|
|
80
|
+
return adapters
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def generate_pyproject(
|
|
84
|
+
project_name: str,
|
|
85
|
+
pipeline: dict[str, Any],
|
|
86
|
+
python_version: str,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Generate pyproject.toml content."""
|
|
89
|
+
adapters = detect_adapters(pipeline)
|
|
90
|
+
|
|
91
|
+
# Base dependencies
|
|
92
|
+
deps = [
|
|
93
|
+
'"hexdag>=0.1.0"',
|
|
94
|
+
'"python-dotenv>=1.0.0"',
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Add adapter-specific dependencies
|
|
98
|
+
if "openai" in adapters.get("llm", []):
|
|
99
|
+
deps.append('"openai>=1.0.0"')
|
|
100
|
+
if "anthropic" in adapters.get("llm", []):
|
|
101
|
+
deps.append('"anthropic>=0.18.0"')
|
|
102
|
+
|
|
103
|
+
deps_str = ",\n ".join(deps)
|
|
104
|
+
|
|
105
|
+
return f'''[project]
|
|
106
|
+
name = "{project_name}"
|
|
107
|
+
version = "0.1.0"
|
|
108
|
+
description = "Auto-generated hexdag pipeline project"
|
|
109
|
+
requires-python = ">={python_version}"
|
|
110
|
+
dependencies = [
|
|
111
|
+
{deps_str}
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[project.scripts]
|
|
115
|
+
{project_name} = "{project_name}.main:main"
|
|
116
|
+
|
|
117
|
+
[build-system]
|
|
118
|
+
requires = ["hatchling"]
|
|
119
|
+
build-backend = "hatchling.build"
|
|
120
|
+
|
|
121
|
+
[tool.hatch.build.targets.wheel]
|
|
122
|
+
packages = ["{project_name}"]
|
|
123
|
+
|
|
124
|
+
[tool.ruff]
|
|
125
|
+
line-length = 100
|
|
126
|
+
target-version = "py312"
|
|
127
|
+
|
|
128
|
+
[tool.ruff.lint]
|
|
129
|
+
select = ["E", "F", "I", "UP"]
|
|
130
|
+
'''
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def generate_readme(
|
|
134
|
+
project_name: str,
|
|
135
|
+
pipeline: dict[str, Any],
|
|
136
|
+
env_vars: list[str],
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Generate README.md content."""
|
|
139
|
+
pipeline_name = pipeline.get("metadata", {}).get("name", project_name)
|
|
140
|
+
description = pipeline.get("metadata", {}).get("description", "A hexdag pipeline project.")
|
|
141
|
+
|
|
142
|
+
nodes = pipeline.get("spec", {}).get("nodes", [])
|
|
143
|
+
node_list = "\n".join([
|
|
144
|
+
f"- **{n.get('metadata', {}).get('name', 'unnamed')}** (`{n.get('kind', 'unknown')}`)"
|
|
145
|
+
for n in nodes
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
env_section = ""
|
|
149
|
+
if env_vars:
|
|
150
|
+
env_list = "\n".join([f"- `{var}`" for var in sorted(env_vars)])
|
|
151
|
+
env_section = f"""
|
|
152
|
+
## Environment Variables
|
|
153
|
+
|
|
154
|
+
The following environment variables are required:
|
|
155
|
+
|
|
156
|
+
{env_list}
|
|
157
|
+
|
|
158
|
+
Copy `.env.example` to `.env` and fill in your values:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cp .env.example .env
|
|
162
|
+
```
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
return f"""# {pipeline_name}
|
|
166
|
+
|
|
167
|
+
{description}
|
|
168
|
+
|
|
169
|
+
## Installation
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Create virtual environment
|
|
173
|
+
python -m venv .venv
|
|
174
|
+
source .venv/bin/activate # On Windows: .venv\\Scripts\\activate
|
|
175
|
+
|
|
176
|
+
# Install dependencies
|
|
177
|
+
pip install -e .
|
|
178
|
+
```
|
|
179
|
+
{env_section}
|
|
180
|
+
## Usage
|
|
181
|
+
|
|
182
|
+
### Run the pipeline
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Using the CLI
|
|
186
|
+
python -m {project_name}.main
|
|
187
|
+
|
|
188
|
+
# Or using the installed script
|
|
189
|
+
{project_name}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Run with custom input
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
import asyncio
|
|
196
|
+
from {project_name}.main import run_pipeline
|
|
197
|
+
|
|
198
|
+
result = asyncio.run(run_pipeline({{"input": "your data here"}}))
|
|
199
|
+
print(result)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Pipeline Structure
|
|
203
|
+
|
|
204
|
+
{node_list}
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Run tests
|
|
210
|
+
pytest
|
|
211
|
+
|
|
212
|
+
# Format code
|
|
213
|
+
ruff format .
|
|
214
|
+
|
|
215
|
+
# Lint
|
|
216
|
+
ruff check .
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
Generated with [hexdag studio](https://github.com/hexdag/hexdag)
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def generate_env_example(env_vars: list[str]) -> str:
|
|
226
|
+
"""Generate .env.example content."""
|
|
227
|
+
if not env_vars:
|
|
228
|
+
return "# No environment variables required\n"
|
|
229
|
+
|
|
230
|
+
lines = ["# Environment variables for this pipeline", ""]
|
|
231
|
+
for var in sorted(env_vars):
|
|
232
|
+
if "KEY" in var or "SECRET" in var or "TOKEN" in var:
|
|
233
|
+
lines.append(f"{var}=your-secret-here")
|
|
234
|
+
elif "URL" in var:
|
|
235
|
+
lines.append(f"{var}=https://api.example.com")
|
|
236
|
+
else:
|
|
237
|
+
lines.append(f"{var}=")
|
|
238
|
+
|
|
239
|
+
return "\n".join(lines) + "\n"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def generate_main_py(project_name: str, pipeline: dict[str, Any]) -> str:
|
|
243
|
+
"""Generate main.py runner script."""
|
|
244
|
+
adapters = detect_adapters(pipeline)
|
|
245
|
+
|
|
246
|
+
# Build imports based on adapters
|
|
247
|
+
imports = [
|
|
248
|
+
"import asyncio",
|
|
249
|
+
"import os",
|
|
250
|
+
"from pathlib import Path",
|
|
251
|
+
"",
|
|
252
|
+
"from dotenv import load_dotenv",
|
|
253
|
+
"",
|
|
254
|
+
"from hexdag.core.pipeline_builder import YamlPipelineBuilder",
|
|
255
|
+
"from hexdag.core.orchestration.orchestrator import Orchestrator",
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
# Add adapter imports
|
|
259
|
+
adapter_imports = []
|
|
260
|
+
if adapters.get("llm"):
|
|
261
|
+
adapter_imports.append("from hexdag.builtin.adapters.openai import OpenAIAdapter")
|
|
262
|
+
if adapters.get("memory"):
|
|
263
|
+
adapter_imports.append("from hexdag.builtin.adapters.memory import InMemoryMemory")
|
|
264
|
+
|
|
265
|
+
if adapter_imports:
|
|
266
|
+
imports.append("")
|
|
267
|
+
imports.extend(adapter_imports)
|
|
268
|
+
|
|
269
|
+
imports_str = "\n".join(imports)
|
|
270
|
+
|
|
271
|
+
# Build port configuration
|
|
272
|
+
ports_config = []
|
|
273
|
+
if adapters.get("llm"):
|
|
274
|
+
ports_config.append(' "llm": OpenAIAdapter(),')
|
|
275
|
+
if adapters.get("memory"):
|
|
276
|
+
ports_config.append(' "memory": InMemoryMemory(),')
|
|
277
|
+
|
|
278
|
+
ports_str = "\n".join(ports_config) if ports_config else " # Add your adapters here"
|
|
279
|
+
|
|
280
|
+
return f'''{imports_str}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Load environment variables
|
|
284
|
+
load_dotenv()
|
|
285
|
+
|
|
286
|
+
# Path to the pipeline YAML
|
|
287
|
+
PIPELINE_PATH = Path(__file__).parent / "pipeline.yaml"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def run_pipeline(inputs: dict | None = None) -> dict:
|
|
291
|
+
"""Run the pipeline with the given inputs.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
inputs: Input data for the pipeline. Defaults to empty dict.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Pipeline execution results.
|
|
298
|
+
"""
|
|
299
|
+
inputs = inputs or {{}}
|
|
300
|
+
|
|
301
|
+
# Build the pipeline from YAML
|
|
302
|
+
builder = YamlPipelineBuilder()
|
|
303
|
+
graph, config = builder.build_from_yaml_file(str(PIPELINE_PATH))
|
|
304
|
+
|
|
305
|
+
# Create orchestrator with adapters
|
|
306
|
+
orchestrator = Orchestrator(
|
|
307
|
+
ports={{
|
|
308
|
+
{ports_str}
|
|
309
|
+
}}
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Run the pipeline
|
|
313
|
+
result = await orchestrator.run(graph, inputs)
|
|
314
|
+
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def main():
|
|
319
|
+
"""CLI entry point."""
|
|
320
|
+
import argparse
|
|
321
|
+
import json
|
|
322
|
+
|
|
323
|
+
parser = argparse.ArgumentParser(description="Run the {project_name} pipeline")
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--input", "-i",
|
|
326
|
+
type=str,
|
|
327
|
+
help="JSON input data for the pipeline",
|
|
328
|
+
default="{{}}"
|
|
329
|
+
)
|
|
330
|
+
parser.add_argument(
|
|
331
|
+
"--input-file", "-f",
|
|
332
|
+
type=str,
|
|
333
|
+
help="Path to JSON file with input data"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
args = parser.parse_args()
|
|
337
|
+
|
|
338
|
+
# Parse input
|
|
339
|
+
if args.input_file:
|
|
340
|
+
with open(args.input_file) as f:
|
|
341
|
+
inputs = json.load(f)
|
|
342
|
+
else:
|
|
343
|
+
inputs = json.loads(args.input)
|
|
344
|
+
|
|
345
|
+
# Run pipeline
|
|
346
|
+
result = asyncio.run(run_pipeline(inputs))
|
|
347
|
+
|
|
348
|
+
# Output result
|
|
349
|
+
print(json.dumps(result, indent=2, default=str))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
main()
|
|
354
|
+
'''
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def generate_init_py(project_name: str) -> str:
|
|
358
|
+
"""Generate __init__.py content."""
|
|
359
|
+
return f'''"""{project_name} - Auto-generated hexdag pipeline."""
|
|
360
|
+
|
|
361
|
+
__version__ = "0.1.0"
|
|
362
|
+
'''
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def generate_gitignore() -> str:
|
|
366
|
+
"""Generate .gitignore content."""
|
|
367
|
+
return """# Python
|
|
368
|
+
__pycache__/
|
|
369
|
+
*.py[cod]
|
|
370
|
+
*$py.class
|
|
371
|
+
*.so
|
|
372
|
+
.Python
|
|
373
|
+
build/
|
|
374
|
+
develop-eggs/
|
|
375
|
+
dist/
|
|
376
|
+
downloads/
|
|
377
|
+
eggs/
|
|
378
|
+
.eggs/
|
|
379
|
+
lib/
|
|
380
|
+
lib64/
|
|
381
|
+
parts/
|
|
382
|
+
sdist/
|
|
383
|
+
var/
|
|
384
|
+
wheels/
|
|
385
|
+
*.egg-info/
|
|
386
|
+
.installed.cfg
|
|
387
|
+
*.egg
|
|
388
|
+
|
|
389
|
+
# Virtual environments
|
|
390
|
+
.venv/
|
|
391
|
+
venv/
|
|
392
|
+
ENV/
|
|
393
|
+
|
|
394
|
+
# IDE
|
|
395
|
+
.idea/
|
|
396
|
+
.vscode/
|
|
397
|
+
*.swp
|
|
398
|
+
*.swo
|
|
399
|
+
|
|
400
|
+
# Environment
|
|
401
|
+
.env
|
|
402
|
+
.env.local
|
|
403
|
+
|
|
404
|
+
# Testing
|
|
405
|
+
.pytest_cache/
|
|
406
|
+
.coverage
|
|
407
|
+
htmlcov/
|
|
408
|
+
|
|
409
|
+
# OS
|
|
410
|
+
.DS_Store
|
|
411
|
+
Thumbs.db
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def generate_dockerfile(project_name: str, python_version: str) -> str:
|
|
416
|
+
"""Generate Dockerfile content."""
|
|
417
|
+
return f'''FROM python:{python_version}-slim
|
|
418
|
+
|
|
419
|
+
WORKDIR /app
|
|
420
|
+
|
|
421
|
+
# Install dependencies
|
|
422
|
+
COPY pyproject.toml .
|
|
423
|
+
RUN pip install --no-cache-dir -e .
|
|
424
|
+
|
|
425
|
+
# Copy application
|
|
426
|
+
COPY . .
|
|
427
|
+
|
|
428
|
+
# Run the pipeline
|
|
429
|
+
CMD ["python", "-m", "{project_name}.main"]
|
|
430
|
+
'''
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@router.post("", response_model=ExportResponse)
|
|
434
|
+
async def export_pipeline(request: ExportRequest) -> ExportResponse:
|
|
435
|
+
"""Export a pipeline as a complete standalone Python project.
|
|
436
|
+
|
|
437
|
+
Generates:
|
|
438
|
+
- pyproject.toml with dependencies
|
|
439
|
+
- README.md with usage instructions
|
|
440
|
+
- .env.example with required environment variables
|
|
441
|
+
- main.py runner script
|
|
442
|
+
- pipeline.yaml (the original pipeline)
|
|
443
|
+
- .gitignore
|
|
444
|
+
- Optionally: Dockerfile
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
# Parse the YAML
|
|
448
|
+
pipeline = safe_load(request.content)
|
|
449
|
+
if not pipeline:
|
|
450
|
+
return ExportResponse(
|
|
451
|
+
success=False, project_name="", files=[], error="Invalid YAML content"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Determine project name
|
|
455
|
+
yaml_name = pipeline.get("metadata", {}).get("name", "pipeline")
|
|
456
|
+
project_name = slugify(request.project_name or yaml_name)
|
|
457
|
+
|
|
458
|
+
# Extract environment variables
|
|
459
|
+
env_vars = extract_env_vars(request.content)
|
|
460
|
+
|
|
461
|
+
# Generate files
|
|
462
|
+
files: list[ExportedFile] = []
|
|
463
|
+
|
|
464
|
+
# pyproject.toml
|
|
465
|
+
files.append(
|
|
466
|
+
ExportedFile(
|
|
467
|
+
path="pyproject.toml",
|
|
468
|
+
content=generate_pyproject(project_name, pipeline, request.python_version),
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# README.md
|
|
473
|
+
files.append(
|
|
474
|
+
ExportedFile(
|
|
475
|
+
path="README.md", content=generate_readme(project_name, pipeline, env_vars)
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# .env.example
|
|
480
|
+
files.append(ExportedFile(path=".env.example", content=generate_env_example(env_vars)))
|
|
481
|
+
|
|
482
|
+
# .gitignore
|
|
483
|
+
files.append(ExportedFile(path=".gitignore", content=generate_gitignore()))
|
|
484
|
+
|
|
485
|
+
# Package directory
|
|
486
|
+
files.append(
|
|
487
|
+
ExportedFile(path=f"{project_name}/__init__.py", content=generate_init_py(project_name))
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# main.py
|
|
491
|
+
files.append(
|
|
492
|
+
ExportedFile(
|
|
493
|
+
path=f"{project_name}/main.py", content=generate_main_py(project_name, pipeline)
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# pipeline.yaml
|
|
498
|
+
files.append(ExportedFile(path=f"{project_name}/pipeline.yaml", content=request.content))
|
|
499
|
+
|
|
500
|
+
# Optional: Dockerfile
|
|
501
|
+
if request.include_docker:
|
|
502
|
+
files.append(
|
|
503
|
+
ExportedFile(
|
|
504
|
+
path="Dockerfile",
|
|
505
|
+
content=generate_dockerfile(project_name, request.python_version),
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return ExportResponse(success=True, project_name=project_name, files=files)
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
return ExportResponse(success=False, project_name="", files=[], error=str(e))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@router.post("/download")
|
|
516
|
+
async def download_project(request: ExportRequest):
|
|
517
|
+
"""Export and download as a ZIP file."""
|
|
518
|
+
import io
|
|
519
|
+
import traceback
|
|
520
|
+
import zipfile
|
|
521
|
+
|
|
522
|
+
from fastapi.responses import StreamingResponse
|
|
523
|
+
|
|
524
|
+
# Validate content
|
|
525
|
+
if not request.content or not request.content.strip():
|
|
526
|
+
return JSONResponse(status_code=400, content={"error": "No YAML content provided"})
|
|
527
|
+
|
|
528
|
+
# Generate the export
|
|
529
|
+
try:
|
|
530
|
+
export_result = await export_pipeline(request)
|
|
531
|
+
except Exception as e:
|
|
532
|
+
traceback.print_exc()
|
|
533
|
+
return JSONResponse(status_code=500, content={"error": f"Export failed: {str(e)}"})
|
|
534
|
+
|
|
535
|
+
if not export_result.success:
|
|
536
|
+
return JSONResponse(
|
|
537
|
+
status_code=400, content={"error": export_result.error or "Unknown error"}
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Create ZIP file in memory
|
|
541
|
+
try:
|
|
542
|
+
zip_buffer = io.BytesIO()
|
|
543
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
544
|
+
for file in export_result.files:
|
|
545
|
+
zf.writestr(f"{export_result.project_name}/{file.path}", file.content)
|
|
546
|
+
|
|
547
|
+
zip_buffer.seek(0)
|
|
548
|
+
|
|
549
|
+
return StreamingResponse(
|
|
550
|
+
zip_buffer,
|
|
551
|
+
media_type="application/zip",
|
|
552
|
+
headers={
|
|
553
|
+
"Content-Disposition": f"attachment; filename={export_result.project_name}.zip"
|
|
554
|
+
},
|
|
555
|
+
)
|
|
556
|
+
except Exception as e:
|
|
557
|
+
traceback.print_exc()
|
|
558
|
+
return JSONResponse(status_code=500, content={"error": f"ZIP creation failed: {str(e)}"})
|