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,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)}"})