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,481 @@
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
+ } from 'lucide-react'
15
+ import { useStudioStore } from '../lib/store'
16
+ import { getAllPluginAdapters, type PluginAdapter } from '../lib/api'
17
+ import yaml from 'yaml'
18
+
19
+ interface PortConfig {
20
+ adapter: string
21
+ config: Record<string, unknown>
22
+ }
23
+
24
+ interface PortsConfig {
25
+ llm?: PortConfig
26
+ memory?: PortConfig
27
+ database?: PortConfig
28
+ storage?: PortConfig
29
+ secret?: PortConfig
30
+ [key: string]: PortConfig | undefined
31
+ }
32
+
33
+ const PORT_TYPES = ['llm', 'memory', 'database', 'storage', 'secret'] as const
34
+
35
+ const PORT_INFO: Record<string, { icon: typeof Brain; color: string; description: string }> = {
36
+ llm: {
37
+ icon: Brain,
38
+ color: '#6366f1',
39
+ description: 'Language model for AI inference (OpenAI, Azure OpenAI, Anthropic)',
40
+ },
41
+ memory: {
42
+ icon: Database,
43
+ color: '#14b8a6',
44
+ description: 'Persistent memory for agent state and conversations',
45
+ },
46
+ database: {
47
+ icon: Database,
48
+ color: '#ec4899',
49
+ description: 'Database connection for data persistence',
50
+ },
51
+ storage: {
52
+ icon: HardDrive,
53
+ color: '#22c55e',
54
+ description: 'File storage for documents and artifacts',
55
+ },
56
+ secret: {
57
+ icon: Key,
58
+ color: '#f59e0b',
59
+ description: 'Secret management for API keys and credentials',
60
+ },
61
+ }
62
+
63
+ export default function PortsEditor() {
64
+ const { yamlContent, setYamlContent, setIsDirty } = useStudioStore()
65
+ const [ports, setPorts] = useState<PortsConfig>({})
66
+ const [adapters, setAdapters] = useState<PluginAdapter[]>([])
67
+ const [isLoading, setIsLoading] = useState(true)
68
+ const [expandedPorts, setExpandedPorts] = useState<Set<string>>(new Set(['llm']))
69
+
70
+ useEffect(() => {
71
+ loadAdapters()
72
+ }, [])
73
+
74
+ useEffect(() => {
75
+ parsePortsFromYaml()
76
+ }, [yamlContent])
77
+
78
+ const loadAdapters = async () => {
79
+ try {
80
+ setIsLoading(true)
81
+ const data = await getAllPluginAdapters()
82
+ setAdapters(data)
83
+ } catch (error) {
84
+ console.error('Failed to load adapters:', error)
85
+ } finally {
86
+ setIsLoading(false)
87
+ }
88
+ }
89
+
90
+ const parsePortsFromYaml = () => {
91
+ try {
92
+ if (!yamlContent) {
93
+ setPorts({})
94
+ return
95
+ }
96
+ const parsed = yaml.parse(yamlContent) as { spec?: { ports?: PortsConfig } }
97
+ setPorts(parsed?.spec?.ports || {})
98
+ } catch {
99
+ setPorts({})
100
+ }
101
+ }
102
+
103
+ const updatePortsInYaml = (newPorts: PortsConfig) => {
104
+ try {
105
+ // Parse existing YAML or create default structure
106
+ let parsed: Record<string, unknown> | null = null
107
+ try {
108
+ parsed = yamlContent ? yaml.parse(yamlContent) as Record<string, unknown> : null
109
+ } catch {
110
+ parsed = null
111
+ }
112
+
113
+ // Create default pipeline structure if needed
114
+ if (!parsed || typeof parsed !== 'object') {
115
+ parsed = {
116
+ apiVersion: 'hexdag/v1',
117
+ kind: 'Pipeline',
118
+ metadata: { name: 'untitled' },
119
+ spec: { nodes: [] },
120
+ }
121
+ }
122
+
123
+ // Ensure spec exists
124
+ if (!parsed.spec || typeof parsed.spec !== 'object') {
125
+ parsed.spec = { nodes: [] }
126
+ }
127
+
128
+ const spec = parsed.spec as { ports?: PortsConfig; nodes?: unknown[] }
129
+
130
+ // Ensure nodes array exists
131
+ if (!spec.nodes) {
132
+ spec.nodes = []
133
+ }
134
+
135
+ // Remove empty ports
136
+ const cleanedPorts: PortsConfig = {}
137
+ for (const [key, value] of Object.entries(newPorts)) {
138
+ if (value && value.adapter) {
139
+ cleanedPorts[key] = value
140
+ }
141
+ }
142
+
143
+ if (Object.keys(cleanedPorts).length > 0) {
144
+ spec.ports = cleanedPorts
145
+ } else {
146
+ delete spec.ports
147
+ }
148
+
149
+ const newYaml = yaml.stringify(parsed, {
150
+ indent: 2,
151
+ lineWidth: 0,
152
+ })
153
+
154
+ setYamlContent(newYaml)
155
+ setIsDirty(true)
156
+ setPorts(cleanedPorts)
157
+ } catch (error) {
158
+ console.error('Failed to update YAML:', error)
159
+ }
160
+ }
161
+
162
+ const togglePort = (portType: string) => {
163
+ setExpandedPorts((prev) => {
164
+ const next = new Set(prev)
165
+ if (next.has(portType)) {
166
+ next.delete(portType)
167
+ } else {
168
+ next.add(portType)
169
+ }
170
+ return next
171
+ })
172
+ }
173
+
174
+ const setPortAdapter = (portType: string, adapterName: string) => {
175
+ const adapter = adapters.find((a) => a.name === adapterName)
176
+ const newPorts = { ...ports }
177
+
178
+ if (!adapterName) {
179
+ delete newPorts[portType]
180
+ } else {
181
+ newPorts[portType] = {
182
+ adapter: adapterName,
183
+ config: adapter ? getDefaultConfig(adapter) : {},
184
+ }
185
+ }
186
+
187
+ updatePortsInYaml(newPorts)
188
+ }
189
+
190
+ const updatePortConfig = (portType: string, key: string, value: unknown) => {
191
+ const newPorts = { ...ports }
192
+ if (newPorts[portType]) {
193
+ newPorts[portType] = {
194
+ ...newPorts[portType]!,
195
+ config: {
196
+ ...newPorts[portType]!.config,
197
+ [key]: value,
198
+ },
199
+ }
200
+ updatePortsInYaml(newPorts)
201
+ }
202
+ }
203
+
204
+ const deletePortConfig = (portType: string, key: string) => {
205
+ const newPorts = { ...ports }
206
+ if (newPorts[portType]) {
207
+ const newConfig = { ...newPorts[portType]!.config }
208
+ delete newConfig[key]
209
+ newPorts[portType] = {
210
+ ...newPorts[portType]!,
211
+ config: newConfig,
212
+ }
213
+ updatePortsInYaml(newPorts)
214
+ }
215
+ }
216
+
217
+ const getDefaultConfig = (adapter: PluginAdapter): Record<string, unknown> => {
218
+ const config: Record<string, unknown> = {}
219
+ const schema = adapter.config_schema as {
220
+ properties?: Record<string, { default?: unknown }>
221
+ required?: string[]
222
+ }
223
+
224
+ if (schema?.properties) {
225
+ for (const [key, prop] of Object.entries(schema.properties)) {
226
+ if (prop.default !== undefined && prop.default !== null) {
227
+ config[key] = prop.default
228
+ } else if (schema.required?.includes(key)) {
229
+ // Add placeholder for required fields
230
+ config[key] = `\${${key.toUpperCase()}}`
231
+ }
232
+ }
233
+ }
234
+
235
+ return config
236
+ }
237
+
238
+ const getAdaptersForPort = (portType: string) => {
239
+ return adapters.filter((a) => a.port_type === portType)
240
+ }
241
+
242
+ if (isLoading) {
243
+ return (
244
+ <div className="h-full flex items-center justify-center">
245
+ <Loader2 size={24} className="animate-spin text-hex-text-muted" />
246
+ </div>
247
+ )
248
+ }
249
+
250
+ return (
251
+ <div className="h-full flex flex-col bg-hex-surface">
252
+ {/* Header */}
253
+ <div className="p-3 border-b border-hex-border">
254
+ <div className="flex items-center gap-2">
255
+ <div className="w-8 h-8 rounded bg-hex-accent/20 flex items-center justify-center">
256
+ <Plug size={16} className="text-hex-accent" />
257
+ </div>
258
+ <div>
259
+ <h3 className="text-sm font-medium text-hex-text">Pipeline Ports</h3>
260
+ <p className="text-[10px] text-hex-text-muted">Configure adapters for your pipeline</p>
261
+ </div>
262
+ </div>
263
+ </div>
264
+
265
+ {/* Port List */}
266
+ <div className="flex-1 overflow-y-auto">
267
+ {PORT_TYPES.map((portType) => {
268
+ const portInfo = PORT_INFO[portType]
269
+ const Icon = portInfo.icon
270
+ const availableAdapters = getAdaptersForPort(portType)
271
+ const currentPort = ports[portType]
272
+ const isExpanded = expandedPorts.has(portType)
273
+ const isConfigured = !!currentPort?.adapter
274
+
275
+ return (
276
+ <div key={portType} className="border-b border-hex-border">
277
+ <button
278
+ onClick={() => togglePort(portType)}
279
+ className="w-full flex items-center gap-2 p-3 hover:bg-hex-border/30 transition-colors"
280
+ >
281
+ {isExpanded ? (
282
+ <ChevronDown size={14} className="text-hex-text-muted" />
283
+ ) : (
284
+ <ChevronRight size={14} className="text-hex-text-muted" />
285
+ )}
286
+ <div
287
+ className="w-6 h-6 rounded flex items-center justify-center"
288
+ style={{ backgroundColor: `${portInfo.color}20` }}
289
+ >
290
+ <Icon size={12} style={{ color: portInfo.color }} />
291
+ </div>
292
+ <span className="flex-1 text-left text-xs font-medium text-hex-text capitalize">
293
+ {portType}
294
+ </span>
295
+ {isConfigured ? (
296
+ <span
297
+ className="text-[10px] px-1.5 py-0.5 rounded"
298
+ style={{ backgroundColor: `${portInfo.color}20`, color: portInfo.color }}
299
+ >
300
+ {currentPort.adapter}
301
+ </span>
302
+ ) : (
303
+ <span className="text-[10px] text-hex-text-muted">Not configured</span>
304
+ )}
305
+ </button>
306
+
307
+ {isExpanded && (
308
+ <div className="px-3 pb-3 space-y-3">
309
+ {/* Description */}
310
+ <div className="flex items-start gap-2 p-2 bg-hex-bg/50 rounded text-[10px] text-hex-text-muted">
311
+ <Info size={12} className="flex-shrink-0 mt-0.5" />
312
+ <span>{portInfo.description}</span>
313
+ </div>
314
+
315
+ {/* Adapter Selection */}
316
+ <div>
317
+ <label className="block text-[10px] font-medium text-hex-text-muted mb-1 uppercase tracking-wider">
318
+ Adapter
319
+ </label>
320
+ {availableAdapters.length === 0 ? (
321
+ <p className="text-[10px] text-hex-text-muted italic">
322
+ No adapters available for this port type
323
+ </p>
324
+ ) : (
325
+ <select
326
+ value={currentPort?.adapter || ''}
327
+ onChange={(e) => setPortAdapter(portType, e.target.value)}
328
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1.5 text-xs text-hex-text focus:border-hex-accent focus:outline-none"
329
+ >
330
+ <option value="">-- Select adapter --</option>
331
+ {availableAdapters.map((adapter) => (
332
+ <option key={adapter.name} value={adapter.name}>
333
+ {adapter.name} ({adapter.plugin})
334
+ </option>
335
+ ))}
336
+ </select>
337
+ )}
338
+ </div>
339
+
340
+ {/* Adapter Config */}
341
+ {currentPort && currentPort.config && (
342
+ <div className="space-y-2">
343
+ <label className="block text-[10px] font-medium text-hex-text-muted uppercase tracking-wider">
344
+ Configuration
345
+ </label>
346
+ {Object.entries(currentPort.config).map(([key, value]) => (
347
+ <ConfigField
348
+ key={key}
349
+ name={key}
350
+ value={value}
351
+ onChange={(v) => updatePortConfig(portType, key, v)}
352
+ onDelete={() => deletePortConfig(portType, key)}
353
+ />
354
+ ))}
355
+ <AddConfigButton
356
+ onAdd={(key) => updatePortConfig(portType, key, '')}
357
+ existingKeys={Object.keys(currentPort.config)}
358
+ />
359
+ </div>
360
+ )}
361
+ </div>
362
+ )}
363
+ </div>
364
+ )
365
+ })}
366
+ </div>
367
+
368
+ {/* Footer */}
369
+ <div className="p-3 border-t border-hex-border bg-hex-bg/50">
370
+ <p className="text-[9px] text-hex-text-muted text-center">
371
+ Ports connect your pipeline to external services like LLMs and databases
372
+ </p>
373
+ </div>
374
+ </div>
375
+ )
376
+ }
377
+
378
+ function ConfigField({
379
+ name,
380
+ value,
381
+ onChange,
382
+ onDelete,
383
+ }: {
384
+ name: string
385
+ value: unknown
386
+ onChange: (value: unknown) => void
387
+ onDelete: () => void
388
+ }) {
389
+ const isSecret = name.toLowerCase().includes('key') ||
390
+ name.toLowerCase().includes('secret') ||
391
+ name.toLowerCase().includes('password') ||
392
+ name.toLowerCase().includes('token')
393
+
394
+ const isEnvVar = typeof value === 'string' && value.startsWith('${') && value.endsWith('}')
395
+
396
+ return (
397
+ <div className="group flex items-start gap-2">
398
+ <div className="flex-1">
399
+ <div className="flex items-center justify-between mb-1">
400
+ <label className="text-[10px] text-hex-text-muted">{name}</label>
401
+ <button
402
+ onClick={onDelete}
403
+ className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-hex-error/20 rounded transition-all"
404
+ >
405
+ <X size={10} className="text-hex-error" />
406
+ </button>
407
+ </div>
408
+ <div className="relative">
409
+ <input
410
+ type={isSecret && !isEnvVar ? 'password' : 'text'}
411
+ value={String(value)}
412
+ onChange={(e) => onChange(e.target.value)}
413
+ 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 font-mono"
414
+ placeholder={isSecret ? '${ENV_VAR_NAME}' : ''}
415
+ />
416
+ {isEnvVar && (
417
+ <span className="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] px-1 py-0.5 rounded bg-amber-500/20 text-amber-500">
418
+ env
419
+ </span>
420
+ )}
421
+ </div>
422
+ </div>
423
+ </div>
424
+ )
425
+ }
426
+
427
+ function AddConfigButton({
428
+ onAdd,
429
+ existingKeys,
430
+ }: {
431
+ onAdd: (key: string) => void
432
+ existingKeys: string[]
433
+ }) {
434
+ const [isAdding, setIsAdding] = useState(false)
435
+ const [newKey, setNewKey] = useState('')
436
+
437
+ const handleAdd = () => {
438
+ if (!newKey || existingKeys.includes(newKey)) return
439
+ onAdd(newKey)
440
+ setNewKey('')
441
+ setIsAdding(false)
442
+ }
443
+
444
+ if (isAdding) {
445
+ return (
446
+ <div className="flex gap-1">
447
+ <input
448
+ type="text"
449
+ value={newKey}
450
+ onChange={(e) => setNewKey(e.target.value)}
451
+ onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
452
+ placeholder="Config key"
453
+ autoFocus
454
+ className="flex-1 bg-hex-bg border border-hex-border rounded px-2 py-1 text-[11px] text-hex-text focus:border-hex-accent focus:outline-none"
455
+ />
456
+ <button
457
+ onClick={handleAdd}
458
+ className="px-2 py-1 text-[10px] bg-hex-accent text-white rounded hover:bg-hex-accent-hover"
459
+ >
460
+ Add
461
+ </button>
462
+ <button
463
+ onClick={() => setIsAdding(false)}
464
+ className="px-2 py-1 text-[10px] bg-hex-border text-hex-text rounded"
465
+ >
466
+ Cancel
467
+ </button>
468
+ </div>
469
+ )
470
+ }
471
+
472
+ return (
473
+ <button
474
+ onClick={() => setIsAdding(true)}
475
+ className="w-full flex items-center justify-center gap-1 py-1 text-[10px] text-hex-text-muted hover:text-hex-text border border-dashed border-hex-border rounded hover:border-hex-accent transition-colors"
476
+ >
477
+ <Plus size={10} />
478
+ Add config
479
+ </button>
480
+ )
481
+ }
@@ -0,0 +1,195 @@
1
+ import { useRef, useCallback } from 'react'
2
+ import Editor, { type OnMount } from '@monaco-editor/react'
3
+ import type * as Monaco from 'monaco-editor'
4
+
5
+ interface PythonEditorProps {
6
+ value: string
7
+ onChange: (value: string) => void
8
+ height?: string | number
9
+ readOnly?: boolean
10
+ }
11
+
12
+ export default function PythonEditor({
13
+ value,
14
+ onChange,
15
+ height = '200px',
16
+ readOnly = false,
17
+ }: PythonEditorProps) {
18
+ const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null)
19
+ const monacoRef = useRef<typeof Monaco | null>(null)
20
+
21
+ const handleEditorMount: OnMount = useCallback((editor, monaco) => {
22
+ editorRef.current = editor
23
+ monacoRef.current = monaco
24
+
25
+ // Define hexdag dark theme for Python
26
+ monaco.editor.defineTheme('hexdag-python-dark', {
27
+ base: 'vs-dark',
28
+ inherit: true,
29
+ rules: [
30
+ { token: 'comment', foreground: '6A9955', fontStyle: 'italic' },
31
+ { token: 'keyword', foreground: 'C586C0' },
32
+ { token: 'string', foreground: 'CE9178' },
33
+ { token: 'number', foreground: 'B5CEA8' },
34
+ { token: 'type', foreground: '4EC9B0' },
35
+ { token: 'function', foreground: 'DCDCAA' },
36
+ { token: 'variable', foreground: '9CDCFE' },
37
+ { token: 'operator', foreground: 'D4D4D4' },
38
+ { token: 'delimiter', foreground: 'D4D4D4' },
39
+ { token: 'decorator', foreground: 'D7BA7D' },
40
+ ],
41
+ colors: {
42
+ 'editor.background': '#0f0f1a',
43
+ 'editor.foreground': '#D4D4D4',
44
+ 'editor.lineHighlightBackground': '#1a1a2e',
45
+ 'editor.selectionBackground': '#264f78',
46
+ 'editorCursor.foreground': '#8B5CF6',
47
+ 'editorLineNumber.foreground': '#4a4a6a',
48
+ 'editorLineNumber.activeForeground': '#8B5CF6',
49
+ 'editor.inactiveSelectionBackground': '#1a1a2e',
50
+ 'editorIndentGuide.background': '#2d2d4a',
51
+ 'editorIndentGuide.activeBackground': '#4a4a6a',
52
+ },
53
+ })
54
+
55
+ monaco.editor.setTheme('hexdag-python-dark')
56
+
57
+ // Add Python snippets
58
+ monaco.languages.registerCompletionItemProvider('python', {
59
+ provideCompletionItems: (model: Monaco.editor.ITextModel, position: Monaco.Position) => {
60
+ const word = model.getWordUntilPosition(position)
61
+ const range = {
62
+ startLineNumber: position.lineNumber,
63
+ endLineNumber: position.lineNumber,
64
+ startColumn: word.startColumn,
65
+ endColumn: word.endColumn,
66
+ }
67
+
68
+ const suggestions: Monaco.languages.CompletionItem[] = [
69
+ {
70
+ label: 'def',
71
+ kind: monaco.languages.CompletionItemKind.Snippet,
72
+ insertText: 'def ${1:function_name}(${2:args}):\n\t${3:pass}',
73
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
74
+ documentation: 'Define a function',
75
+ range,
76
+ },
77
+ {
78
+ label: 'async def',
79
+ kind: monaco.languages.CompletionItemKind.Snippet,
80
+ insertText: 'async def ${1:function_name}(${2:args}):\n\t${3:pass}',
81
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
82
+ documentation: 'Define an async function',
83
+ range,
84
+ },
85
+ {
86
+ label: 'lambda',
87
+ kind: monaco.languages.CompletionItemKind.Snippet,
88
+ insertText: 'lambda ${1:x}: ${2:x}',
89
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
90
+ documentation: 'Lambda expression',
91
+ range,
92
+ },
93
+ {
94
+ label: 'class',
95
+ kind: monaco.languages.CompletionItemKind.Snippet,
96
+ insertText: 'class ${1:ClassName}:\n\tdef __init__(self${2:, args}):\n\t\t${3:pass}',
97
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
98
+ documentation: 'Define a class',
99
+ range,
100
+ },
101
+ {
102
+ label: 'for',
103
+ kind: monaco.languages.CompletionItemKind.Snippet,
104
+ insertText: 'for ${1:item} in ${2:items}:\n\t${3:pass}',
105
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
106
+ documentation: 'For loop',
107
+ range,
108
+ },
109
+ {
110
+ label: 'if',
111
+ kind: monaco.languages.CompletionItemKind.Snippet,
112
+ insertText: 'if ${1:condition}:\n\t${2:pass}',
113
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
114
+ documentation: 'If statement',
115
+ range,
116
+ },
117
+ {
118
+ label: 'try',
119
+ kind: monaco.languages.CompletionItemKind.Snippet,
120
+ insertText: 'try:\n\t${1:pass}\nexcept ${2:Exception} as e:\n\t${3:raise}',
121
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
122
+ documentation: 'Try/except block',
123
+ range,
124
+ },
125
+ {
126
+ label: 'with',
127
+ kind: monaco.languages.CompletionItemKind.Snippet,
128
+ insertText: 'with ${1:context} as ${2:var}:\n\t${3:pass}',
129
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
130
+ documentation: 'With statement',
131
+ range,
132
+ },
133
+ ]
134
+
135
+ return { suggestions }
136
+ },
137
+ })
138
+ }, [])
139
+
140
+ const handleChange = useCallback(
141
+ (newValue: string | undefined) => {
142
+ if (newValue !== undefined) {
143
+ onChange(newValue)
144
+ }
145
+ },
146
+ [onChange]
147
+ )
148
+
149
+ return (
150
+ <div className="w-full border border-hex-border rounded overflow-hidden">
151
+ <Editor
152
+ height={height}
153
+ defaultLanguage="python"
154
+ value={value}
155
+ onChange={handleChange}
156
+ onMount={handleEditorMount}
157
+ options={{
158
+ minimap: { enabled: false },
159
+ fontSize: 12,
160
+ fontFamily: '"JetBrains Mono", "Fira Code", monospace',
161
+ lineNumbers: 'on',
162
+ scrollBeyondLastLine: false,
163
+ automaticLayout: true,
164
+ tabSize: 4,
165
+ insertSpaces: true,
166
+ wordWrap: 'on',
167
+ readOnly,
168
+ scrollbar: {
169
+ vertical: 'auto',
170
+ horizontal: 'auto',
171
+ verticalScrollbarSize: 8,
172
+ horizontalScrollbarSize: 8,
173
+ },
174
+ padding: {
175
+ top: 8,
176
+ bottom: 8,
177
+ },
178
+ renderLineHighlight: 'line',
179
+ cursorBlinking: 'smooth',
180
+ cursorSmoothCaretAnimation: 'on',
181
+ smoothScrolling: true,
182
+ contextmenu: true,
183
+ bracketPairColorization: {
184
+ enabled: true,
185
+ },
186
+ guides: {
187
+ bracketPairs: true,
188
+ indentation: true,
189
+ },
190
+ }}
191
+ theme="hexdag-python-dark"
192
+ />
193
+ </div>
194
+ )
195
+ }