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,441 @@
1
+ """Plugins management command for HexDAG CLI."""
2
+
3
+ import contextlib
4
+ from enum import StrEnum
5
+ from typing import Annotated, Any
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
+ class OutputFormat(StrEnum):
16
+ """Output format options."""
17
+
18
+ TABLE = "table"
19
+ JSON = "json"
20
+ YAML = "yaml"
21
+
22
+
23
+ @app.command("list")
24
+ def list_plugins(
25
+ format: Annotated[
26
+ OutputFormat | None,
27
+ typer.Option(
28
+ "--format",
29
+ "-f",
30
+ help="Output format",
31
+ ),
32
+ ] = None,
33
+ ) -> None:
34
+ """List all available plugins and adapters."""
35
+ if format is None:
36
+ format = OutputFormat.TABLE
37
+
38
+ # Check available extras
39
+ available_plugins = _get_available_plugins()
40
+
41
+ if format == OutputFormat.JSON:
42
+ console.print_json(data=available_plugins)
43
+ elif format == OutputFormat.YAML:
44
+ import yaml
45
+
46
+ console.print(yaml.dump(available_plugins, default_flow_style=False))
47
+ else:
48
+ # Table format
49
+ table = Table(title="Available Plugins", show_header=True, header_style="bold magenta")
50
+ table.add_column("Plugin", style="cyan", no_wrap=True)
51
+ table.add_column("Namespace", style="green")
52
+ table.add_column("Status", style="yellow")
53
+ table.add_column("Capabilities", style="white")
54
+
55
+ for plugin in available_plugins:
56
+ status = "✓ Installed" if plugin["installed"] else "✗ Not installed"
57
+ table.add_row(
58
+ str(plugin["name"]),
59
+ str(plugin["namespace"]),
60
+ status,
61
+ ", ".join(str(c) for c in plugin["capabilities"]),
62
+ )
63
+
64
+ console.print(table)
65
+
66
+
67
+ @app.command("check")
68
+ def check_plugins() -> None:
69
+ """Check plugin dependencies and suggest installation commands."""
70
+ console.print("[bold]Checking plugin dependencies...[/bold]\n")
71
+
72
+ checks = _check_dependencies()
73
+ has_missing = False
74
+
75
+ for check in checks:
76
+ if check["status"] == "ok":
77
+ console.print(f"✓ [green]{check['name']}[/green] - OK")
78
+ elif check["status"] == "missing":
79
+ has_missing = True
80
+ console.print(f"✗ [red]{check['name']}[/red] - Missing")
81
+ if check.get("install_hint"):
82
+ console.print(f" → Install with: [yellow]{check['install_hint']}[/yellow]")
83
+ elif check["status"] == "optional":
84
+ console.print(f"○ [yellow]{check['name']}[/yellow] - Optional")
85
+ if check.get("install_hint"):
86
+ console.print(f" → Install with: [dim]{check['install_hint']}[/dim]")
87
+
88
+ if not has_missing:
89
+ console.print("\n[green]All required dependencies are installed![/green]")
90
+ else:
91
+ console.print(
92
+ "\n[yellow]Some dependencies are missing. See installation hints above.[/yellow]"
93
+ )
94
+
95
+
96
+ @app.command("install")
97
+ def install_plugin(
98
+ plugin_name: str = typer.Argument(
99
+ ...,
100
+ help="Plugin name or extra to install (e.g., 'openai', 'viz')",
101
+ ),
102
+ use_uv: bool = typer.Option(
103
+ True,
104
+ "--uv/--pip",
105
+ help="Prefer uv when available; use --pip to force pip",
106
+ ),
107
+ dry_run: bool = typer.Option(
108
+ False,
109
+ "--dry-run",
110
+ help="Show what would be installed without actually installing",
111
+ ),
112
+ editable: bool = typer.Option(
113
+ False,
114
+ "--editable",
115
+ "-e",
116
+ help="Install in editable/development mode",
117
+ ),
118
+ ) -> None:
119
+ """Install a plugin or adapter (wrapper around package manager).
120
+
121
+ Raises
122
+ ------
123
+ typer.Exit
124
+ If installation fails or plugin not found
125
+ """
126
+ # Map plugin names to extras
127
+ plugin_map = {
128
+ "openai": "adapters-openai",
129
+ "anthropic": "adapters-anthropic",
130
+ "viz": "viz",
131
+ "visualization": "viz",
132
+ "cli": "cli",
133
+ "all": "all",
134
+ }
135
+
136
+ extra = plugin_map.get(plugin_name, plugin_name)
137
+
138
+ # Detect package manager
139
+ import shutil
140
+ import subprocess # nosec B404 - Required for package installation
141
+ from pathlib import Path
142
+
143
+ # Determine which package manager to use
144
+ has_uv = shutil.which("uv") is not None
145
+ use_uv_final = use_uv and has_uv
146
+
147
+ if use_uv and not has_uv:
148
+ console.print("[yellow]Warning: uv requested but not found. Using pip instead.[/yellow]")
149
+ use_uv_final = False
150
+
151
+ if use_uv_final:
152
+ if editable:
153
+ # For editable install with uv, need to install from current directory
154
+ if Path("pyproject.toml").exists():
155
+ cmd_list = ["uv", "pip", "install", "-e", f".[{extra}]"]
156
+ cmd_str = f"uv pip install -e .[{extra}]"
157
+ else:
158
+ console.print(
159
+ "[red]Error: Editable install requires pyproject.toml "
160
+ "in current directory[/red]"
161
+ )
162
+ raise typer.Exit(1)
163
+ else:
164
+ cmd_list = ["uv", "pip", "install", f"hexdag[{extra}]"]
165
+ cmd_str = f"uv pip install hexdag[{extra}]"
166
+ else:
167
+ if editable:
168
+ if Path("pyproject.toml").exists():
169
+ cmd_list = ["pip", "install", "-e", f".[{extra}]"]
170
+ cmd_str = f"pip install -e .[{extra}]"
171
+ else:
172
+ console.print(
173
+ "[red]Error: Editable install requires pyproject.toml "
174
+ "in current directory[/red]"
175
+ )
176
+ raise typer.Exit(1)
177
+ else:
178
+ cmd_list = ["pip", "install", f"hexdag[{extra}]"]
179
+ cmd_str = f"pip install hexdag[{extra}]"
180
+
181
+ if dry_run:
182
+ # Use markup=False to avoid bracket interpretation
183
+ console.print("[yellow]Would run:[/yellow] ", end="")
184
+ console.print(cmd_str, markup=False)
185
+ if use_uv_final:
186
+ console.print("[dim]Using: uv package manager[/dim]")
187
+ else:
188
+ console.print("[dim]Using: pip package manager[/dim]")
189
+ else:
190
+ console.print(f"[cyan]Installing {plugin_name}...[/cyan]")
191
+ # Use markup=False for the command
192
+ console.print("Running: [bold]", end="")
193
+ console.print(cmd_str, markup=False, style="bold")
194
+ console.print("") # Empty line
195
+
196
+ # Run the installation - using list format without shell=True for security
197
+ result = subprocess.run(cmd_list, capture_output=True, text=True) # nosec B603
198
+
199
+ if result.returncode == 0:
200
+ console.print(f"[green]✓ Successfully installed {plugin_name}[/green]")
201
+
202
+ # Show what was installed
203
+ if "Successfully installed" in result.stdout:
204
+ console.print("\n[dim]Installed packages:[/dim]")
205
+ for line in result.stdout.split("\n"):
206
+ if "Successfully installed" in line:
207
+ packages = line.split("Successfully installed")[1].strip()
208
+ for pkg in packages.split():
209
+ console.print(f" • {pkg}")
210
+ else:
211
+ console.print(f"[red]✗ Failed to install {plugin_name}[/red]")
212
+ if result.stderr:
213
+ console.print(f"[dim]{result.stderr}[/dim]")
214
+ if result.stdout and "error" in result.stdout.lower():
215
+ console.print(f"[dim]{result.stdout}[/dim]")
216
+
217
+
218
+ def _get_available_plugins() -> list[dict[str, Any]]:
219
+ """Get list of available plugins by scanning known module paths."""
220
+ import importlib
221
+
222
+ plugins = []
223
+
224
+ # Built-in adapters (always available)
225
+ builtin_adapters = {
226
+ "mock": {
227
+ "module": "hexdag.builtin.adapters.mock",
228
+ "capabilities": ["LLM", "Database", "ToolRouter"],
229
+ "namespace": "core",
230
+ },
231
+ "memory": {
232
+ "module": "hexdag.builtin.adapters.memory",
233
+ "capabilities": ["Memory"],
234
+ "namespace": "core",
235
+ },
236
+ }
237
+
238
+ for name, info in builtin_adapters.items():
239
+ installed = False
240
+ with contextlib.suppress(ImportError):
241
+ importlib.import_module(info["module"])
242
+ installed = True
243
+
244
+ plugins.append({
245
+ "name": name,
246
+ "namespace": info["namespace"],
247
+ "installed": installed,
248
+ "capabilities": info["capabilities"],
249
+ })
250
+
251
+ # Optional adapters (extras)
252
+ optional_adapters = {
253
+ "openai": {
254
+ "module": "hexdag.adapters.openai",
255
+ "capabilities": ["LLM", "Embeddings"],
256
+ "namespace": "plugin",
257
+ },
258
+ "anthropic": {
259
+ "module": "hexdag.adapters.anthropic",
260
+ "capabilities": ["LLM"],
261
+ "namespace": "plugin",
262
+ },
263
+ "visualization": {
264
+ "module": "hexdag.visualization",
265
+ "capabilities": ["DAG Visualization"],
266
+ "namespace": "core",
267
+ },
268
+ }
269
+
270
+ for name, info in optional_adapters.items():
271
+ installed = False
272
+ with contextlib.suppress(ImportError):
273
+ importlib.import_module(info["module"])
274
+ installed = True
275
+
276
+ plugins.append({
277
+ "name": name,
278
+ "namespace": info["namespace"],
279
+ "installed": installed,
280
+ "capabilities": info["capabilities"],
281
+ })
282
+
283
+ # Check for plugins in hexdag_plugins directory
284
+ from pathlib import Path
285
+
286
+ # Try to find hexdag_plugins
287
+ current = Path.cwd()
288
+ plugin_dir = None
289
+ while current != current.parent:
290
+ if (current / "hexdag_plugins").exists():
291
+ plugin_dir = current / "hexdag_plugins"
292
+ break
293
+ current = current.parent
294
+
295
+ if plugin_dir:
296
+ for plugin_path in plugin_dir.iterdir():
297
+ if plugin_path.is_dir() and not plugin_path.name.startswith((".", "_")):
298
+ plugin_name = plugin_path.name
299
+ # Skip if already in list
300
+ if plugin_name in [p["name"] for p in plugins]:
301
+ continue
302
+
303
+ # Try to determine capabilities from pyproject.toml
304
+ capabilities = ["Adapter"]
305
+ pyproject = plugin_path / "pyproject.toml"
306
+ if pyproject.exists():
307
+ import tomllib
308
+
309
+ with pyproject.open("rb") as f:
310
+ data = tomllib.load(f)
311
+ # Check for port hints in keywords
312
+ keywords = data.get("project", {}).get("keywords", [])
313
+ for kw in keywords:
314
+ if kw in ("llm", "database", "memory", "api"):
315
+ capabilities = [kw.upper() if kw != "llm" else "LLM"]
316
+
317
+ # Check if installable
318
+ installed = False
319
+ try:
320
+ importlib.import_module(f"hexdag_plugins.{plugin_name}")
321
+ installed = True
322
+ except ImportError:
323
+ pass
324
+
325
+ plugins.append({
326
+ "name": plugin_name,
327
+ "namespace": "plugin",
328
+ "installed": installed,
329
+ "capabilities": capabilities,
330
+ })
331
+
332
+ # Special check for visualization graphviz support
333
+ try:
334
+ from hexdag.visualization import GRAPHVIZ_AVAILABLE
335
+
336
+ for plugin in plugins:
337
+ if plugin["name"] == "visualization":
338
+ plugin["installed"] = GRAPHVIZ_AVAILABLE
339
+ break
340
+ except ImportError:
341
+ pass
342
+
343
+ return plugins
344
+
345
+
346
+ def _check_dependencies() -> list[dict]:
347
+ """Check plugin dependencies dynamically."""
348
+ import importlib
349
+ import shutil
350
+
351
+ # Detect if uv is available for better hints
352
+ has_uv = shutil.which("uv") is not None
353
+ prefix = "uv pip install" if has_uv else "pip install"
354
+
355
+ checks = []
356
+
357
+ # Define dependency checks
358
+ dependency_checks = {
359
+ "pydantic": {
360
+ "name": "pydantic (core)",
361
+ "required": True,
362
+ "extra": None,
363
+ },
364
+ "yaml": {
365
+ "name": "PyYAML (CLI)",
366
+ "required": False,
367
+ "extra": "cli",
368
+ },
369
+ "graphviz": {
370
+ "name": "graphviz (visualization)",
371
+ "required": False,
372
+ "extra": "viz",
373
+ },
374
+ }
375
+
376
+ # Check each dependency
377
+ for module_name, info in dependency_checks.items():
378
+ try:
379
+ importlib.import_module(module_name)
380
+ checks.append({"name": info["name"], "status": "ok"})
381
+ except ImportError:
382
+ status = "missing" if info["required"] else "optional"
383
+ hint = f"{prefix} hexdag"
384
+ if info["extra"]:
385
+ hint = f"{prefix} hexdag[{info['extra']}]"
386
+
387
+ checks.append({
388
+ "name": info["name"],
389
+ "status": status,
390
+ "install_hint": hint,
391
+ })
392
+
393
+ # Check optional adapter packages dynamically
394
+ adapter_packages: dict[str, Any] = {
395
+ "openai": {
396
+ "display": "OpenAI",
397
+ "adapter_module": "hexdag.adapters.openai",
398
+ "extra": "adapters-openai",
399
+ },
400
+ "anthropic": {
401
+ "display": "Anthropic",
402
+ "adapter_module": "hexdag.adapters.anthropic",
403
+ "extra": "adapters-anthropic",
404
+ },
405
+ }
406
+
407
+ for sdk_name, info in adapter_packages.items():
408
+ sdk_ok = False
409
+ adapter_ok = False
410
+
411
+ # Check SDK
412
+ try:
413
+ importlib.import_module(sdk_name)
414
+ sdk_ok = True
415
+ except ImportError:
416
+ pass
417
+
418
+ # Check adapter if SDK exists
419
+ if sdk_ok:
420
+ try:
421
+ importlib.import_module(info["adapter_module"])
422
+ adapter_ok = True
423
+ except ImportError:
424
+ pass
425
+
426
+ if sdk_ok and adapter_ok:
427
+ checks.append({"name": f"{info['display']} (SDK + Adapter)", "status": "ok"})
428
+ elif sdk_ok:
429
+ checks.append({
430
+ "name": f"{info['display']} (SDK only, adapter missing)",
431
+ "status": "optional",
432
+ "install_hint": f"{prefix} hexdag[{info['extra']}]",
433
+ })
434
+ else:
435
+ checks.append({
436
+ "name": f"{info['display']} (not installed)",
437
+ "status": "optional",
438
+ "install_hint": f"{prefix} hexdag[{info['extra']}]",
439
+ })
440
+
441
+ return checks
@@ -0,0 +1,101 @@
1
+ """hexdag studio - Local-first visual editor for pipelines.
2
+
3
+ Usage:
4
+ hexdag studio ./pipelines/
5
+ hexdag studio --port 8080
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ app = typer.Typer(name="studio", help="Local-first visual editor for pipelines")
14
+ console = Console()
15
+
16
+
17
+ @app.callback(invoke_without_command=True)
18
+ def studio(
19
+ ctx: typer.Context,
20
+ path: Path = typer.Argument(
21
+ Path(),
22
+ help="Directory containing pipeline YAML files",
23
+ exists=True,
24
+ file_okay=False,
25
+ dir_okay=True,
26
+ resolve_path=True,
27
+ ),
28
+ host: str = typer.Option(
29
+ "127.0.0.1",
30
+ "--host",
31
+ "-h",
32
+ help="Host to bind to",
33
+ ),
34
+ port: int = typer.Option(
35
+ 3141,
36
+ "--port",
37
+ "-p",
38
+ help="Port to bind to",
39
+ ),
40
+ no_browser: bool = typer.Option(
41
+ False,
42
+ "--no-browser",
43
+ help="Don't open browser automatically",
44
+ ),
45
+ ) -> None:
46
+ """Start the hexdag studio visual editor.
47
+
48
+ Opens a browser-based editor that reads/writes YAML files directly.
49
+ No cloud, no accounts - just local files that work with git.
50
+
51
+ Examples:
52
+ hexdag studio ./pipelines/
53
+ hexdag studio --port 8080
54
+ hexdag studio . --no-browser
55
+ """
56
+ # Check for studio dependencies
57
+ try:
58
+ import fastapi # noqa: F401
59
+ import uvicorn # noqa: F401
60
+ except ImportError:
61
+ console.print(
62
+ "[red]Error:[/red] Studio dependencies not installed.\n"
63
+ "Please install with:\n"
64
+ " [cyan]pip install hexdag[studio][/cyan]\n"
65
+ " or\n"
66
+ " [cyan]uv pip install hexdag[studio][/cyan]"
67
+ )
68
+ raise typer.Exit(code=1)
69
+
70
+ # Resolve path
71
+ workspace = path.resolve()
72
+
73
+ # Print startup banner
74
+ console.print()
75
+ console.print("[bold blue]hexdag studio[/bold blue] v0.1.0")
76
+ console.print()
77
+ console.print(f" [dim]Workspace:[/dim] {workspace}")
78
+ console.print(
79
+ f" [dim]Local:[/dim] [link=http://{host}:{port}]http://{host}:{port}[/link]"
80
+ )
81
+ console.print()
82
+ console.print(" [dim]Press Ctrl+C to stop[/dim]")
83
+ console.print()
84
+
85
+ # Open browser
86
+ if not no_browser:
87
+ import threading
88
+ import webbrowser
89
+
90
+ def open_browser() -> None:
91
+ import time
92
+
93
+ time.sleep(1) # Wait for server to start
94
+ webbrowser.open(f"http://{host}:{port}")
95
+
96
+ threading.Thread(target=open_browser, daemon=True).start()
97
+
98
+ # Start server
99
+ from hexdag.studio.server.main import run_server
100
+
101
+ run_server(workspace, host=host, port=port)