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,262 @@
1
+ import { useState, useEffect } from 'react'
2
+ import {
3
+ Code,
4
+ Brain,
5
+ Cpu,
6
+ FileText,
7
+ Scissors,
8
+ Bot,
9
+ GitBranch,
10
+ Repeat,
11
+ Box,
12
+ GripVertical,
13
+ Plug,
14
+ ChevronDown,
15
+ ChevronRight,
16
+ Loader2,
17
+ Package,
18
+ FileInput,
19
+ FileOutput,
20
+ Table,
21
+ Mail,
22
+ Send,
23
+ } from 'lucide-react'
24
+ import { nodeTemplates } from '../lib/nodeTemplates'
25
+ import { getAllPluginNodes, type PluginNode } from '../lib/api'
26
+
27
+ const iconMap: Record<string, typeof Code> = {
28
+ Code,
29
+ Brain,
30
+ Cpu,
31
+ FileText,
32
+ Scissors,
33
+ Bot,
34
+ GitBranch,
35
+ Repeat,
36
+ Box,
37
+ Plug,
38
+ Package,
39
+ }
40
+
41
+ interface PluginNodeSection {
42
+ plugin: string
43
+ nodes: PluginNode[]
44
+ expanded: boolean
45
+ }
46
+
47
+ export default function NodePalette() {
48
+ const [pluginNodeSections, setPluginNodeSections] = useState<PluginNodeSection[]>([])
49
+ const [isLoading, setIsLoading] = useState(true)
50
+ const [builtinExpanded, setBuiltinExpanded] = useState(true)
51
+
52
+ useEffect(() => {
53
+ loadPlugins()
54
+ }, [])
55
+
56
+ const loadPlugins = async () => {
57
+ try {
58
+ setIsLoading(true)
59
+ const nodes = await getAllPluginNodes()
60
+
61
+ // Group nodes by plugin
62
+ const groupedNodes = nodes.reduce(
63
+ (acc, node) => {
64
+ const plugin = node.plugin || 'unknown'
65
+ if (!acc[plugin]) {
66
+ acc[plugin] = []
67
+ }
68
+ acc[plugin].push(node)
69
+ return acc
70
+ },
71
+ {} as Record<string, PluginNode[]>
72
+ )
73
+
74
+ setPluginNodeSections(
75
+ Object.entries(groupedNodes).map(([plugin, nodes]) => ({
76
+ plugin,
77
+ nodes,
78
+ expanded: true,
79
+ }))
80
+ )
81
+ } catch (error) {
82
+ console.error('Failed to load plugins:', error)
83
+ } finally {
84
+ setIsLoading(false)
85
+ }
86
+ }
87
+
88
+ const toggleNodeSection = (plugin: string) => {
89
+ setPluginNodeSections((sections) =>
90
+ sections.map((s) => (s.plugin === plugin ? { ...s, expanded: !s.expanded } : s))
91
+ )
92
+ }
93
+
94
+ const onDragStart = (event: React.DragEvent, kind: string) => {
95
+ event.dataTransfer.setData('application/hexdag-node', kind)
96
+ event.dataTransfer.effectAllowed = 'move'
97
+ }
98
+
99
+ const getPluginNodeIcon = (node: PluginNode) => {
100
+ const kindLower = node.kind.toLowerCase()
101
+ if (kindLower.includes('file_reader') || kindLower.includes('input')) return FileInput
102
+ if (kindLower.includes('file_writer') || kindLower.includes('output')) return FileOutput
103
+ if (kindLower.includes('outlook_reader') || kindLower.includes('mail_reader')) return Mail
104
+ if (kindLower.includes('outlook_sender') || kindLower.includes('mail_sender')) return Send
105
+ if (kindLower.includes('transform') || kindLower.includes('pandas')) return Table
106
+ if (kindLower.includes('llm') || kindLower.includes('openai')) return Brain
107
+ if (kindLower.includes('database') || kindLower.includes('cosmos')) return Cpu
108
+ if (kindLower.includes('storage') || kindLower.includes('blob')) return FileText
109
+ return Package
110
+ }
111
+
112
+ return (
113
+ <div className="h-full flex flex-col">
114
+ <div className="p-3 border-b border-hex-border">
115
+ <h2 className="text-xs font-semibold uppercase text-hex-text-muted tracking-wider">
116
+ Node Palette
117
+ </h2>
118
+ </div>
119
+ <div className="flex-1 overflow-y-auto">
120
+ {/* Built-in Nodes Section */}
121
+ <div className="border-b border-hex-border">
122
+ <button
123
+ onClick={() => setBuiltinExpanded(!builtinExpanded)}
124
+ className="w-full flex items-center gap-2 p-2 hover:bg-hex-border/30 transition-colors"
125
+ >
126
+ {builtinExpanded ? (
127
+ <ChevronDown size={14} className="text-hex-text-muted" />
128
+ ) : (
129
+ <ChevronRight size={14} className="text-hex-text-muted" />
130
+ )}
131
+ <Box size={14} className="text-hex-accent" />
132
+ <span className="text-xs font-medium text-hex-text">Built-in Nodes</span>
133
+ <span className="ml-auto text-[10px] text-hex-text-muted">{nodeTemplates.length}</span>
134
+ </button>
135
+ {builtinExpanded && (
136
+ <div className="px-2 pb-2 space-y-1">
137
+ {nodeTemplates.map((template) => {
138
+ const Icon = iconMap[template.icon] || Box
139
+ return (
140
+ <div
141
+ key={template.kind}
142
+ draggable
143
+ onDragStart={(e) => onDragStart(e, template.kind)}
144
+ className="
145
+ flex items-center gap-2 p-2 rounded-md cursor-grab
146
+ bg-hex-bg hover:bg-hex-border/50 transition-colors
147
+ border border-transparent hover:border-hex-border
148
+ group
149
+ "
150
+ >
151
+ <div className="text-hex-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
152
+ <GripVertical size={12} />
153
+ </div>
154
+ <div
155
+ className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0"
156
+ style={{ backgroundColor: `${template.color}20` }}
157
+ >
158
+ <Icon size={14} style={{ color: template.color }} />
159
+ </div>
160
+ <div className="flex-1 min-w-0">
161
+ <div className="text-xs font-medium text-hex-text truncate">
162
+ {template.label}
163
+ </div>
164
+ <div className="text-[10px] text-hex-text-muted truncate">
165
+ {template.description}
166
+ </div>
167
+ </div>
168
+ </div>
169
+ )
170
+ })}
171
+ </div>
172
+ )}
173
+ </div>
174
+
175
+ {/* Plugin Nodes Sections */}
176
+ {isLoading ? (
177
+ <div className="flex items-center justify-center p-4">
178
+ <Loader2 size={16} className="animate-spin text-hex-text-muted" />
179
+ <span className="ml-2 text-xs text-hex-text-muted">Loading plugins...</span>
180
+ </div>
181
+ ) : (
182
+ <>
183
+ {pluginNodeSections.map((section) => (
184
+ <div key={section.plugin} className="border-b border-hex-border">
185
+ <button
186
+ onClick={() => toggleNodeSection(section.plugin)}
187
+ className="w-full flex items-center gap-2 p-2 hover:bg-hex-border/30 transition-colors"
188
+ >
189
+ {section.expanded ? (
190
+ <ChevronDown size={14} className="text-hex-text-muted" />
191
+ ) : (
192
+ <ChevronRight size={14} className="text-hex-text-muted" />
193
+ )}
194
+ <Plug size={14} className="text-green-500" />
195
+ <span className="text-xs font-medium text-hex-text truncate">
196
+ {section.plugin}
197
+ </span>
198
+ <span className="ml-auto text-[10px] text-hex-text-muted">
199
+ {section.nodes.length}
200
+ </span>
201
+ </button>
202
+ {section.expanded && (
203
+ <div className="px-2 pb-2 space-y-1">
204
+ {section.nodes.map((node) => {
205
+ const Icon = getPluginNodeIcon(node)
206
+ return (
207
+ <div
208
+ key={`${section.plugin}:${node.kind}`}
209
+ draggable
210
+ onDragStart={(e) => onDragStart(e, node.kind)}
211
+ className="
212
+ flex items-center gap-2 p-2 rounded-md cursor-grab
213
+ bg-hex-bg hover:bg-hex-border/50 transition-colors
214
+ border border-transparent hover:border-hex-border
215
+ group
216
+ "
217
+ >
218
+ <div className="text-hex-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
219
+ <GripVertical size={12} />
220
+ </div>
221
+ <div
222
+ className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0"
223
+ style={{ backgroundColor: `${node.color}20` }}
224
+ >
225
+ <Icon size={14} style={{ color: node.color }} />
226
+ </div>
227
+ <div className="flex-1 min-w-0">
228
+ <div className="text-xs font-medium text-hex-text truncate">
229
+ {node.name}
230
+ </div>
231
+ <div className="text-[10px] text-hex-text-muted truncate">
232
+ {node.description || `From ${section.plugin}`}
233
+ </div>
234
+ </div>
235
+ </div>
236
+ )
237
+ })}
238
+ </div>
239
+ )}
240
+ </div>
241
+ ))}
242
+
243
+ {pluginNodeSections.length === 0 && (
244
+ <div className="p-4 text-center">
245
+ <Plug size={24} className="mx-auto mb-2 text-hex-text-muted opacity-50" />
246
+ <p className="text-xs text-hex-text-muted">No plugin nodes found</p>
247
+ <p className="text-[10px] text-hex-text-muted mt-1">
248
+ Add node plugins to hexdag_plugins/
249
+ </p>
250
+ </div>
251
+ )}
252
+ </>
253
+ )}
254
+ </div>
255
+ <div className="p-3 border-t border-hex-border">
256
+ <p className="text-[10px] text-hex-text-muted text-center">
257
+ Drag nodes onto the canvas
258
+ </p>
259
+ </div>
260
+ </div>
261
+ )
262
+ }
@@ -0,0 +1,403 @@
1
+ import { useState, useEffect } from 'react'
2
+ import {
3
+ Brain,
4
+ Key,
5
+ Database,
6
+ HardDrive,
7
+ Plug,
8
+ ChevronDown,
9
+ ChevronRight,
10
+ Plus,
11
+ X,
12
+ Loader2,
13
+ Info,
14
+ Wrench,
15
+ } from 'lucide-react'
16
+ import { getAllPluginAdapters, type PluginAdapter } from '../lib/api'
17
+
18
+ interface PortConfig {
19
+ adapter: string
20
+ config: Record<string, unknown>
21
+ }
22
+
23
+ type PortsConfig = Record<string, PortConfig | undefined>
24
+
25
+ interface NodePortsSectionProps {
26
+ nodeId: string
27
+ requiredPorts: string[]
28
+ nodePorts: PortsConfig
29
+ onPortsChange: (ports: PortsConfig) => void
30
+ }
31
+
32
+ const PORT_INFO: Record<string, { icon: typeof Brain; color: string; description: string }> = {
33
+ llm: {
34
+ icon: Brain,
35
+ color: '#6366f1',
36
+ description: 'Language model for this node',
37
+ },
38
+ memory: {
39
+ icon: Database,
40
+ color: '#14b8a6',
41
+ description: 'Memory for this node',
42
+ },
43
+ database: {
44
+ icon: Database,
45
+ color: '#ec4899',
46
+ description: 'Database for this node',
47
+ },
48
+ storage: {
49
+ icon: HardDrive,
50
+ color: '#22c55e',
51
+ description: 'Storage for this node',
52
+ },
53
+ secret: {
54
+ icon: Key,
55
+ color: '#f59e0b',
56
+ description: 'Secrets for this node',
57
+ },
58
+ tool_router: {
59
+ icon: Wrench,
60
+ color: '#8b5cf6',
61
+ description: 'Tool router for agent tools',
62
+ },
63
+ }
64
+
65
+ export default function NodePortsSection({
66
+ nodeId: _nodeId, // For future use (per-node tracking)
67
+ requiredPorts,
68
+ nodePorts,
69
+ onPortsChange,
70
+ }: NodePortsSectionProps) {
71
+ const [adapters, setAdapters] = useState<PluginAdapter[]>([])
72
+ const [isLoading, setIsLoading] = useState(true)
73
+ const [expandedPorts, setExpandedPorts] = useState<Set<string>>(new Set(requiredPorts))
74
+
75
+ useEffect(() => {
76
+ loadAdapters()
77
+ }, [])
78
+
79
+ const loadAdapters = async () => {
80
+ try {
81
+ setIsLoading(true)
82
+ const data = await getAllPluginAdapters()
83
+ setAdapters(data)
84
+ } catch (error) {
85
+ console.error('Failed to load adapters:', error)
86
+ } finally {
87
+ setIsLoading(false)
88
+ }
89
+ }
90
+
91
+ const togglePort = (portType: string) => {
92
+ setExpandedPorts((prev) => {
93
+ const next = new Set(prev)
94
+ if (next.has(portType)) {
95
+ next.delete(portType)
96
+ } else {
97
+ next.add(portType)
98
+ }
99
+ return next
100
+ })
101
+ }
102
+
103
+ const setPortAdapter = (portType: string, adapterName: string) => {
104
+ const adapter = adapters.find((a) => a.name === adapterName)
105
+ const newPorts = { ...nodePorts }
106
+
107
+ if (!adapterName) {
108
+ delete newPorts[portType]
109
+ } else {
110
+ newPorts[portType] = {
111
+ adapter: adapterName,
112
+ config: adapter ? getDefaultConfig(adapter) : {},
113
+ }
114
+ }
115
+
116
+ onPortsChange(newPorts)
117
+ }
118
+
119
+ const updatePortConfig = (portType: string, key: string, value: unknown) => {
120
+ const newPorts = { ...nodePorts }
121
+ if (newPorts[portType]) {
122
+ newPorts[portType] = {
123
+ ...newPorts[portType]!,
124
+ config: {
125
+ ...newPorts[portType]!.config,
126
+ [key]: value,
127
+ },
128
+ }
129
+ onPortsChange(newPorts)
130
+ }
131
+ }
132
+
133
+ const deletePortConfig = (portType: string, key: string) => {
134
+ const newPorts = { ...nodePorts }
135
+ if (newPorts[portType]) {
136
+ const newConfig = { ...newPorts[portType]!.config }
137
+ delete newConfig[key]
138
+ newPorts[portType] = {
139
+ ...newPorts[portType]!,
140
+ config: newConfig,
141
+ }
142
+ onPortsChange(newPorts)
143
+ }
144
+ }
145
+
146
+ const getDefaultConfig = (adapter: PluginAdapter): Record<string, unknown> => {
147
+ const config: Record<string, unknown> = {}
148
+ const schema = adapter.config_schema as {
149
+ properties?: Record<string, { default?: unknown }>
150
+ required?: string[]
151
+ }
152
+
153
+ if (schema?.properties) {
154
+ for (const [key, prop] of Object.entries(schema.properties)) {
155
+ if (prop.default !== undefined && prop.default !== null) {
156
+ config[key] = prop.default
157
+ } else if (schema.required?.includes(key)) {
158
+ config[key] = `\${${key.toUpperCase()}}`
159
+ }
160
+ }
161
+ }
162
+
163
+ return config
164
+ }
165
+
166
+ const getAdaptersForPort = (portType: string) => {
167
+ return adapters.filter((a) => a.port_type === portType)
168
+ }
169
+
170
+ if (requiredPorts.length === 0) {
171
+ return null
172
+ }
173
+
174
+ if (isLoading) {
175
+ return (
176
+ <div className="flex items-center justify-center py-4">
177
+ <Loader2 size={16} className="animate-spin text-hex-text-muted" />
178
+ </div>
179
+ )
180
+ }
181
+
182
+ return (
183
+ <div className="space-y-2">
184
+ {/* Info banner */}
185
+ <div className="flex items-start gap-2 p-2 bg-hex-accent/10 rounded text-[10px] text-hex-accent">
186
+ <Info size={12} className="flex-shrink-0 mt-0.5" />
187
+ <span>
188
+ Override pipeline-level ports for this node. Leave empty to use global configuration.
189
+ </span>
190
+ </div>
191
+
192
+ {/* Port List */}
193
+ {requiredPorts.map((portType) => {
194
+ const portInfo = PORT_INFO[portType] || {
195
+ icon: Plug,
196
+ color: '#6b7280',
197
+ description: `${portType} port`,
198
+ }
199
+ const Icon = portInfo.icon
200
+ const availableAdapters = getAdaptersForPort(portType)
201
+ const currentPort = nodePorts[portType]
202
+ const isExpanded = expandedPorts.has(portType)
203
+ const isConfigured = !!currentPort?.adapter
204
+
205
+ return (
206
+ <div key={portType} className="border border-hex-border rounded overflow-hidden">
207
+ <button
208
+ onClick={() => togglePort(portType)}
209
+ className="w-full flex items-center gap-2 p-2 hover:bg-hex-border/30 transition-colors"
210
+ >
211
+ {isExpanded ? (
212
+ <ChevronDown size={12} className="text-hex-text-muted" />
213
+ ) : (
214
+ <ChevronRight size={12} className="text-hex-text-muted" />
215
+ )}
216
+ <div
217
+ className="w-5 h-5 rounded flex items-center justify-center"
218
+ style={{ backgroundColor: `${portInfo.color}20` }}
219
+ >
220
+ <Icon size={10} style={{ color: portInfo.color }} />
221
+ </div>
222
+ <span className="flex-1 text-left text-[11px] font-medium text-hex-text capitalize">
223
+ {portType.replace('_', ' ')}
224
+ </span>
225
+ {isConfigured ? (
226
+ <span
227
+ className="text-[9px] px-1.5 py-0.5 rounded"
228
+ style={{ backgroundColor: `${portInfo.color}20`, color: portInfo.color }}
229
+ >
230
+ {currentPort.adapter}
231
+ </span>
232
+ ) : (
233
+ <span className="text-[9px] text-hex-text-muted italic">Global</span>
234
+ )}
235
+ </button>
236
+
237
+ {isExpanded && (
238
+ <div className="px-2 pb-2 space-y-2 border-t border-hex-border bg-hex-bg/30">
239
+ {/* Description */}
240
+ <div className="flex items-start gap-2 p-1.5 text-[9px] text-hex-text-muted">
241
+ <span>{portInfo.description}</span>
242
+ </div>
243
+
244
+ {/* Adapter Selection */}
245
+ <div>
246
+ <label className="block text-[9px] font-medium text-hex-text-muted mb-1 uppercase tracking-wider">
247
+ Adapter Override
248
+ </label>
249
+ {availableAdapters.length === 0 ? (
250
+ <p className="text-[9px] text-hex-text-muted italic">
251
+ No adapters available for {portType}
252
+ </p>
253
+ ) : (
254
+ <select
255
+ value={currentPort?.adapter || ''}
256
+ onChange={(e) => setPortAdapter(portType, e.target.value)}
257
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1 text-[11px] text-hex-text focus:border-hex-accent focus:outline-none"
258
+ >
259
+ <option value="">-- Use global config --</option>
260
+ {availableAdapters.map((adapter) => (
261
+ <option key={adapter.name} value={adapter.name}>
262
+ {adapter.name} ({adapter.plugin})
263
+ </option>
264
+ ))}
265
+ </select>
266
+ )}
267
+ </div>
268
+
269
+ {/* Adapter Config */}
270
+ {currentPort && currentPort.config && Object.keys(currentPort.config).length > 0 && (
271
+ <div className="space-y-1.5">
272
+ <label className="block text-[9px] font-medium text-hex-text-muted uppercase tracking-wider">
273
+ Configuration
274
+ </label>
275
+ {Object.entries(currentPort.config).map(([key, value]) => (
276
+ <ConfigField
277
+ key={key}
278
+ name={key}
279
+ value={value}
280
+ onChange={(v) => updatePortConfig(portType, key, v)}
281
+ onDelete={() => deletePortConfig(portType, key)}
282
+ />
283
+ ))}
284
+ <AddConfigButton
285
+ onAdd={(key) => updatePortConfig(portType, key, '')}
286
+ existingKeys={Object.keys(currentPort.config)}
287
+ />
288
+ </div>
289
+ )}
290
+ </div>
291
+ )}
292
+ </div>
293
+ )
294
+ })}
295
+ </div>
296
+ )
297
+ }
298
+
299
+ function ConfigField({
300
+ name,
301
+ value,
302
+ onChange,
303
+ onDelete,
304
+ }: {
305
+ name: string
306
+ value: unknown
307
+ onChange: (value: unknown) => void
308
+ onDelete: () => void
309
+ }) {
310
+ const isSecret =
311
+ name.toLowerCase().includes('key') ||
312
+ name.toLowerCase().includes('secret') ||
313
+ name.toLowerCase().includes('password') ||
314
+ name.toLowerCase().includes('token')
315
+
316
+ const isEnvVar = typeof value === 'string' && value.startsWith('${') && value.endsWith('}')
317
+
318
+ return (
319
+ <div className="group flex items-start gap-1.5">
320
+ <div className="flex-1">
321
+ <div className="flex items-center justify-between mb-0.5">
322
+ <label className="text-[9px] text-hex-text-muted">{name}</label>
323
+ <button
324
+ onClick={onDelete}
325
+ className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-hex-error/20 rounded transition-all"
326
+ >
327
+ <X size={8} className="text-hex-error" />
328
+ </button>
329
+ </div>
330
+ <div className="relative">
331
+ <input
332
+ type={isSecret && !isEnvVar ? 'password' : 'text'}
333
+ value={String(value)}
334
+ onChange={(e) => onChange(e.target.value)}
335
+ className="w-full bg-hex-bg border border-hex-border rounded px-1.5 py-0.5 text-[10px] text-hex-text focus:border-hex-accent focus:outline-none font-mono"
336
+ placeholder={isSecret ? '${ENV_VAR}' : ''}
337
+ />
338
+ {isEnvVar && (
339
+ <span className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[8px] px-1 py-0.5 rounded bg-amber-500/20 text-amber-500">
340
+ env
341
+ </span>
342
+ )}
343
+ </div>
344
+ </div>
345
+ </div>
346
+ )
347
+ }
348
+
349
+ function AddConfigButton({
350
+ onAdd,
351
+ existingKeys,
352
+ }: {
353
+ onAdd: (key: string) => void
354
+ existingKeys: string[]
355
+ }) {
356
+ const [isAdding, setIsAdding] = useState(false)
357
+ const [newKey, setNewKey] = useState('')
358
+
359
+ const handleAdd = () => {
360
+ if (!newKey || existingKeys.includes(newKey)) return
361
+ onAdd(newKey)
362
+ setNewKey('')
363
+ setIsAdding(false)
364
+ }
365
+
366
+ if (isAdding) {
367
+ return (
368
+ <div className="flex gap-1">
369
+ <input
370
+ type="text"
371
+ value={newKey}
372
+ onChange={(e) => setNewKey(e.target.value)}
373
+ onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
374
+ placeholder="Config key"
375
+ autoFocus
376
+ className="flex-1 bg-hex-bg border border-hex-border rounded px-1.5 py-0.5 text-[10px] text-hex-text focus:border-hex-accent focus:outline-none"
377
+ />
378
+ <button
379
+ onClick={handleAdd}
380
+ className="px-1.5 py-0.5 text-[9px] bg-hex-accent text-white rounded hover:bg-hex-accent-hover"
381
+ >
382
+ Add
383
+ </button>
384
+ <button
385
+ onClick={() => setIsAdding(false)}
386
+ className="px-1.5 py-0.5 text-[9px] bg-hex-border text-hex-text rounded"
387
+ >
388
+ Cancel
389
+ </button>
390
+ </div>
391
+ )
392
+ }
393
+
394
+ return (
395
+ <button
396
+ onClick={() => setIsAdding(true)}
397
+ className="w-full flex items-center justify-center gap-1 py-0.5 text-[9px] text-hex-text-muted hover:text-hex-text border border-dashed border-hex-border rounded hover:border-hex-accent transition-colors"
398
+ >
399
+ <Plus size={8} />
400
+ Add config
401
+ </button>
402
+ )
403
+ }