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
hexdag/__init__.py ADDED
@@ -0,0 +1,116 @@
1
+ """Hex-DAG Agent Framework.
2
+
3
+ A modular, deterministic, and extensible architecture for orchestrating LLM-powered agents alongside
4
+ traditional code with YAML pipeline configuration.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ # Version is defined in pyproject.toml and read dynamically
10
+ try:
11
+ from importlib.metadata import version
12
+
13
+ __version__ = version("hexdag")
14
+ except Exception:
15
+ __version__ = "0.0.0.dev0" # Fallback for development installs
16
+
17
+ # Core framework exports
18
+ from hexdag.builtin.nodes import (
19
+ ConditionalNode,
20
+ FunctionNode,
21
+ LLMNode,
22
+ LoopNode,
23
+ ReActAgentNode,
24
+ )
25
+ from hexdag.core.domain import DirectedGraph, NodeSpec
26
+ from hexdag.core.orchestration.orchestrator import Orchestrator
27
+ from hexdag.core.orchestration.prompt import FewShotPromptTemplate, PromptTemplate
28
+
29
+ # YAML Workflow Builder
30
+ from hexdag.core.pipeline_builder.yaml_builder import YamlPipelineBuilder
31
+
32
+ # Port interfaces
33
+ from hexdag.core.ports import LLM, APICall, DatabasePort, ToolRouter
34
+
35
+ # Import resolver for component resolution
36
+ from hexdag.core.resolver import resolve, resolve_function
37
+
38
+ # Define placeholders for lazy-loaded adapters to satisfy __all__ checking
39
+ # These will be replaced by __getattr__ when accessed
40
+ if TYPE_CHECKING:
41
+ from hexdag.builtin.adapters.memory import InMemoryMemory
42
+ from hexdag.builtin.adapters.mock import MockDatabaseAdapter, MockLLM
43
+
44
+
45
+ # Lazy loading for adapters and optional modules to avoid circular imports
46
+ def __getattr__(name: str) -> Any:
47
+ """Lazy import for adapters and optional components.
48
+
49
+ Raises
50
+ ------
51
+ ImportError
52
+ If visualization module is not available
53
+ AttributeError
54
+ If the requested attribute does not exist
55
+ """
56
+ # Mock adapters
57
+ if name == "MockLLM":
58
+ from hexdag.builtin.adapters.mock import MockLLM as _MockLLM
59
+
60
+ return _MockLLM
61
+ if name == "MockDatabaseAdapter":
62
+ from hexdag.builtin.adapters.mock import MockDatabaseAdapter as _MockDatabaseAdapter
63
+
64
+ return _MockDatabaseAdapter
65
+ if name == "MockToolRouter":
66
+ from hexdag.builtin.adapters.mock import MockToolRouter as _MockToolRouter
67
+
68
+ return _MockToolRouter
69
+
70
+ # Visualization components (optional)
71
+ if name == "DAGVisualizer":
72
+ try:
73
+ from hexdag.visualization import DAGVisualizer as _DAGVisualizer
74
+
75
+ return _DAGVisualizer
76
+ except ImportError as e:
77
+ raise ImportError(
78
+ "Visualization module not available. Install with:\n"
79
+ " pip install hexdag[viz]\n"
80
+ " or\n"
81
+ " uv pip install hexdag[viz]"
82
+ ) from e
83
+
84
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
85
+
86
+
87
+ __all__ = [
88
+ # Version
89
+ "__version__",
90
+ # Module Resolution
91
+ "resolve",
92
+ "resolve_function",
93
+ # Core Framework - DAG Building and Execution
94
+ "Orchestrator",
95
+ "DirectedGraph",
96
+ "NodeSpec",
97
+ "YamlPipelineBuilder",
98
+ # Node Factories
99
+ "FunctionNode",
100
+ "LLMNode",
101
+ "ReActAgentNode",
102
+ "LoopNode",
103
+ "ConditionalNode",
104
+ # Templating System
105
+ "PromptTemplate",
106
+ "FewShotPromptTemplate",
107
+ # Port Interfaces
108
+ "LLM",
109
+ "APICall",
110
+ "ToolRouter",
111
+ "DatabasePort",
112
+ # Testing and Development Adapters
113
+ "InMemoryMemory",
114
+ "MockLLM",
115
+ "MockDatabaseAdapter",
116
+ ]
hexdag/__main__.py ADDED
@@ -0,0 +1,30 @@
1
+ """Entry point for running hexDAG as a module.
2
+
3
+ Supports both CLI and MCP server modes:
4
+ - python -m hexdag [CLI command]
5
+ - python -m hexdag --mcp (starts MCP server)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+
13
+ def main() -> None:
14
+ """Main entry point for module execution."""
15
+ # Check if --mcp flag is present
16
+ if "--mcp" in sys.argv:
17
+ # Remove --mcp flag and run MCP server
18
+ sys.argv.remove("--mcp")
19
+ from hexdag.mcp_server import mcp
20
+
21
+ mcp.run()
22
+ else:
23
+ # Run regular CLI
24
+ from hexdag.cli import main as cli_main
25
+
26
+ cli_main()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
@@ -0,0 +1,5 @@
1
+ """Executor adapters for different execution strategies."""
2
+
3
+ from hexdag.adapters.executors.local_executor import LocalExecutor
4
+
5
+ __all__ = ["LocalExecutor"]
@@ -0,0 +1,316 @@
1
+ """Local in-process executor adapter.
2
+
3
+ This executor runs nodes in the same process using asyncio, providing
4
+ the same behavior as the built-in orchestrator execution but through
5
+ the ExecutorPort interface.
6
+ """
7
+
8
+ import asyncio
9
+ import time
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from hexdag.core.context import get_port
13
+ from hexdag.core.logging import get_logger
14
+ from hexdag.core.orchestration.components import (
15
+ ExecutionCoordinator,
16
+ NodeExecutionError,
17
+ NodeExecutor,
18
+ )
19
+ from hexdag.core.orchestration.constants import (
20
+ EXECUTOR_CONTEXT_GRAPH,
21
+ EXECUTOR_CONTEXT_INITIAL_INPUT,
22
+ EXECUTOR_CONTEXT_NODE_RESULTS,
23
+ )
24
+ from hexdag.core.orchestration.hook_context import PipelineStatus
25
+ from hexdag.core.orchestration.models import NodeExecutionContext
26
+ from hexdag.core.ports.executor import (
27
+ ExecutionResult,
28
+ ExecutionTask,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from hexdag.core.domain.dag import DirectedGraph, NodeSpec
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ def _calculate_duration_ms(start_time: float) -> float:
38
+ """Calculate duration in milliseconds from start time.
39
+
40
+ Parameters
41
+ ----------
42
+ start_time : float
43
+ Start time from time.time()
44
+
45
+ Returns
46
+ -------
47
+ float
48
+ Duration in milliseconds
49
+ """
50
+ return (time.time() - start_time) * 1000
51
+
52
+
53
+ class Local:
54
+ """Configuration for LocalExecutor.
55
+
56
+ Attributes
57
+ ----------
58
+ max_concurrent_nodes : int
59
+ Maximum number of nodes to execute concurrently (default: 10)
60
+ strict_validation : bool
61
+ If True, raise errors on validation failure (default: False)
62
+ default_node_timeout : float | None
63
+ Default timeout in seconds for each node (default: None, no timeout)
64
+ """
65
+
66
+ max_concurrent_nodes: int = 10
67
+ strict_validation: bool = False
68
+ default_node_timeout: float | None = None
69
+
70
+
71
+ class LocalExecutor:
72
+ """Local in-process executor using asyncio.
73
+
74
+ This executor wraps the core NodeExecutor and WaveExecutor to provide
75
+ the same in-process execution behavior but through the ExecutorPort
76
+ interface. It's useful for:
77
+
78
+ 1. Testing the executor abstraction
79
+ 2. Providing a consistent interface across execution strategies
80
+ 3. Serving as a reference implementation for other executors
81
+
82
+ The LocalExecutor maintains the same features as built-in execution:
83
+ - Async/sync function support
84
+ - Validation and type checking
85
+ - Timeout handling
86
+ - Event emission
87
+ - Policy evaluation
88
+
89
+ Examples
90
+ --------
91
+ Basic usage::
92
+
93
+ executor = LocalExecutor(max_concurrent_nodes=5)
94
+ orchestrator = Orchestrator(executor=executor)
95
+ results = await orchestrator.run(graph, input_data)
96
+
97
+ With validation and timeout::
98
+
99
+ executor = LocalExecutor(
100
+ strict_validation=True,
101
+ default_node_timeout=30.0
102
+ )
103
+ orchestrator = Orchestrator(executor=executor)
104
+
105
+ From YAML::
106
+
107
+ executor:
108
+ type: local
109
+ namespace: core
110
+ params:
111
+ max_concurrent_nodes: 20
112
+ strict_validation: true
113
+ default_node_timeout: 60.0
114
+ """
115
+
116
+ def __init__(
117
+ self, strict_validation: bool = True, default_node_timeout: float = 60.0, **kwargs: Any
118
+ ) -> None:
119
+ """Initialize local executor.
120
+
121
+ Parameters
122
+ ----------
123
+ strict_validation : bool, default=True
124
+ Enable strict validation
125
+ default_node_timeout : float, default=60.0
126
+ Default timeout for nodes in seconds
127
+ """
128
+ self.strict_validation = strict_validation
129
+ self.default_node_timeout = default_node_timeout
130
+ self.max_concurrent_nodes = kwargs.get("max_concurrent_nodes", 10)
131
+
132
+ # Core execution components
133
+ self._node_executor = NodeExecutor(
134
+ strict_validation=strict_validation,
135
+ default_node_timeout=default_node_timeout,
136
+ )
137
+ self._execution_coordinator = ExecutionCoordinator()
138
+
139
+ # State for tracking
140
+ self._initialized = False
141
+
142
+ async def aexecute_node(self, task: ExecutionTask) -> ExecutionResult:
143
+ """Execute a single node in-process.
144
+
145
+ Parameters
146
+ ----------
147
+ task : ExecutionTask
148
+ Task containing node information and input data
149
+
150
+ Returns
151
+ -------
152
+ ExecutionResult
153
+ Result of the execution with output or error information
154
+
155
+ Raises
156
+ ------
157
+ RuntimeError
158
+ If executor not initialized via asetup()
159
+
160
+ Notes
161
+ -----
162
+ This method expects that the orchestrator has set up the execution
163
+ context with the graph and ports. The task.context_data should contain
164
+ the necessary execution context information.
165
+ """
166
+ if not self._initialized:
167
+ raise RuntimeError("LocalExecutor not initialized - call asetup() first")
168
+
169
+ start_time = time.time()
170
+
171
+ try:
172
+ # Note: Uses ContextVar pattern consistent with rest of codebase
173
+ graph: DirectedGraph = get_port(EXECUTOR_CONTEXT_GRAPH)
174
+ node_results: dict[str, Any] = get_port(EXECUTOR_CONTEXT_NODE_RESULTS)
175
+ initial_input: Any = get_port(EXECUTOR_CONTEXT_INITIAL_INPUT)
176
+
177
+ if graph is None:
178
+ raise RuntimeError(
179
+ "LocalExecutor requires execution context with graph. "
180
+ "Ensure orchestrator has set up context properly."
181
+ )
182
+
183
+ if task.node_name not in graph.nodes:
184
+ return ExecutionResult(
185
+ node_name=task.node_name,
186
+ status=PipelineStatus.FAILED,
187
+ error=f"Node '{task.node_name}' not found in graph",
188
+ error_type="KeyError",
189
+ duration_ms=_calculate_duration_ms(start_time),
190
+ )
191
+
192
+ node_spec: NodeSpec = graph.nodes[task.node_name]
193
+
194
+ # Prepare input using ExecutionCoordinator
195
+ node_input = self._execution_coordinator.prepare_node_input(
196
+ node_spec, node_results, initial_input
197
+ )
198
+
199
+ execution_context = NodeExecutionContext(
200
+ dag_id=task.context_data.get("dag_id", "unnamed"),
201
+ node_id=task.node_name,
202
+ wave_index=task.wave_index,
203
+ attempt=task.context_data.get("attempt", 1),
204
+ metadata=task.context_data,
205
+ )
206
+
207
+ # Execute using NodeExecutor
208
+ output = await self._node_executor.execute_node(
209
+ node_name=task.node_name,
210
+ node_spec=node_spec,
211
+ node_input=node_input,
212
+ context=execution_context,
213
+ coordinator=self._execution_coordinator,
214
+ wave_index=task.wave_index,
215
+ validate=task.should_validate,
216
+ **task.params,
217
+ )
218
+
219
+ return ExecutionResult(
220
+ node_name=task.node_name,
221
+ output=output,
222
+ status=PipelineStatus.SUCCESS,
223
+ duration_ms=_calculate_duration_ms(start_time),
224
+ )
225
+
226
+ except NodeExecutionError:
227
+ # NodeExecutionError should propagate directly (orchestrator will handle)
228
+ raise
229
+ except (ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
230
+ # Catch expected execution errors (validation, type issues, missing data)
231
+ logger.error(f"LocalExecutor: Node '{task.node_name}' failed: {e}")
232
+ return ExecutionResult(
233
+ node_name=task.node_name,
234
+ status=PipelineStatus.FAILED,
235
+ error=str(e),
236
+ error_type=type(e).__name__,
237
+ duration_ms=_calculate_duration_ms(start_time),
238
+ )
239
+ except Exception as e:
240
+ # Catch unexpected errors but log with higher severity
241
+ logger.exception(f"LocalExecutor: Unexpected error in node '{task.node_name}': {e}")
242
+ return ExecutionResult(
243
+ node_name=task.node_name,
244
+ status=PipelineStatus.FAILED,
245
+ error=str(e),
246
+ error_type=type(e).__name__,
247
+ duration_ms=_calculate_duration_ms(start_time),
248
+ )
249
+
250
+ async def aexecute_wave(self, tasks: list[ExecutionTask]) -> dict[str, ExecutionResult]:
251
+ """Execute multiple nodes concurrently within a wave.
252
+
253
+ Parameters
254
+ ----------
255
+ tasks : list[ExecutionTask]
256
+ List of tasks to execute in parallel
257
+
258
+ Returns
259
+ -------
260
+ dict[str, ExecutionResult]
261
+ Map of node_name -> execution result
262
+
263
+ Raises
264
+ ------
265
+ RuntimeError
266
+ If executor not initialized via asetup()
267
+
268
+ Notes
269
+ -----
270
+ This method uses asyncio.gather to execute all tasks concurrently,
271
+ respecting the max_concurrent_nodes limit via semaphore.
272
+ """
273
+ if not self._initialized:
274
+ raise RuntimeError("LocalExecutor not initialized - call asetup() first")
275
+
276
+ semaphore = asyncio.Semaphore(self.max_concurrent_nodes)
277
+
278
+ async def execute_with_limit(task: ExecutionTask) -> ExecutionResult:
279
+ """Execute task with semaphore-based concurrency control."""
280
+ async with semaphore:
281
+ return await self.aexecute_node(task)
282
+
283
+ # Execute all tasks concurrently with semaphore limit
284
+ results_list = await asyncio.gather(
285
+ *[execute_with_limit(task) for task in tasks],
286
+ return_exceptions=True,
287
+ )
288
+
289
+ results = {}
290
+ for result in results_list:
291
+ if isinstance(result, ExecutionResult):
292
+ results[result.node_name] = result
293
+ elif isinstance(result, Exception):
294
+ # Can happen if task is cancelled during gather()
295
+ logger.error(f"Exception during wave execution: {result}")
296
+ raise result
297
+
298
+ return results
299
+
300
+ async def asetup(self) -> None:
301
+ """Initialize executor resources.
302
+
303
+ For LocalExecutor, this is a no-op as all resources are
304
+ in-process and don't require setup.
305
+ """
306
+ self._initialized = True
307
+ logger.debug("LocalExecutor initialized")
308
+
309
+ async def aclose(self) -> None:
310
+ """Cleanup executor resources.
311
+
312
+ For LocalExecutor, this is a no-op as all resources are
313
+ in-process and don't require cleanup.
314
+ """
315
+ self._initialized = False
316
+ logger.debug("LocalExecutor closed")
@@ -0,0 +1,6 @@
1
+ """Built-in plugins for hexDAG framework.
2
+
3
+ This module contains built-in implementations of nodes, adapters, tools,
4
+ policies, and observers that ship with hexDAG. All components in this
5
+ module are registrable plugins with namespace="core".
6
+ """
@@ -0,0 +1,51 @@
1
+ """Initalize the hexdag adapter package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from .unified_tool_router import UnifiedToolRouter # noqa: F401
10
+
11
+ _LAZY_MAP: dict[str, tuple[str, str]] = {
12
+ "UnifiedToolRouter": (
13
+ "hexdag.adapters.unified_tool_router",
14
+ "UnifiedToolRouter",
15
+ ),
16
+ # Backward compatibility alias
17
+ "FunctionBasedToolRouter": (
18
+ "hexdag.adapters.unified_tool_router",
19
+ "UnifiedToolRouter",
20
+ ),
21
+ }
22
+
23
+ __all__: tuple[str, ...] = tuple(_LAZY_MAP)
24
+
25
+
26
+ def __getattr__(name: str) -> Any:
27
+ """Lazy import for adapters.
28
+
29
+ Returns
30
+ -------
31
+ Any
32
+ The imported adapter class or module.
33
+
34
+ Raises
35
+ ------
36
+ AttributeError
37
+ If the requested name is not found in the lazy import map.
38
+ """
39
+ if name in _LAZY_MAP:
40
+ module_name, attr = _LAZY_MAP[name]
41
+ module = importlib.import_module(module_name)
42
+ value = getattr(module, attr)
43
+
44
+ # Register the port dynamically if necessary
45
+ if hasattr(value, "register_port"):
46
+ value.register_port()
47
+
48
+ globals()[name] = value # Cache the imported module in the globals for future access
49
+ return value
50
+
51
+ raise AttributeError(f"module {__name__} has no attribute {name}")
@@ -0,0 +1,5 @@
1
+ """Anthropic adapters for hexDAG."""
2
+
3
+ from hexdag.builtin.adapters.anthropic.anthropic_adapter import AnthropicAdapter
4
+
5
+ __all__ = ["AnthropicAdapter"]
@@ -0,0 +1,151 @@
1
+ """Anthropic adapter for LLM interactions."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ from anthropic import AsyncAnthropic
7
+
8
+ from hexdag.core.logging import get_logger
9
+ from hexdag.core.ports.llm import MessageList
10
+ from hexdag.core.types import (
11
+ PositiveInt,
12
+ RetryCount,
13
+ Temperature01,
14
+ TimeoutSeconds,
15
+ TopP,
16
+ )
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class AnthropicAdapter:
22
+ """Anthropic implementation of the LLM port.
23
+
24
+ This adapter provides integration with Anthropic's Claude models through
25
+ their API. It supports async operations and handles message conversion
26
+ between hexDAG's format and Anthropic's format.
27
+
28
+ Secret Management
29
+ -----------------
30
+ API key resolution order:
31
+ 1. Explicit parameter: AnthropicAdapter(api_key="sk-...")
32
+ 2. Environment variable: ANTHROPIC_API_KEY
33
+ 3. Memory port (orchestrator): secret:ANTHROPIC_API_KEY
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str | None = None,
39
+ model: str = "claude-3-5-sonnet-20241022",
40
+ temperature: Temperature01 = 0.7,
41
+ max_tokens: PositiveInt = 4096,
42
+ top_p: TopP = 1.0,
43
+ top_k: PositiveInt | None = None,
44
+ system_prompt: str | None = None,
45
+ timeout: TimeoutSeconds = 60.0,
46
+ max_retries: RetryCount = 2,
47
+ **kwargs: Any, # ← For extra params like base_url
48
+ ):
49
+ """Initialize Anthropic adapter.
50
+
51
+ Parameters
52
+ ----------
53
+ api_key : str | None
54
+ Anthropic API key (auto-resolved from ANTHROPIC_API_KEY env var if not provided)
55
+ model : str, default="claude-3-5-sonnet-20241022"
56
+ Claude model to use
57
+ temperature : float, default=0.7
58
+ Sampling temperature (0-1)
59
+ max_tokens : int, default=4096
60
+ Maximum tokens in response
61
+ top_p : float, default=1.0
62
+ Nucleus sampling parameter
63
+ top_k : int | None, default=None
64
+ Top-k sampling parameter
65
+ system_prompt : str | None, default=None
66
+ System prompt to use
67
+ timeout : float, default=60.0
68
+ Request timeout in seconds
69
+ max_retries : int, default=2
70
+ Maximum retry attempts
71
+ """
72
+ self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
73
+ if not self.api_key:
74
+ raise ValueError("api_key required (pass directly or set ANTHROPIC_API_KEY)")
75
+ self.model = model
76
+ self.temperature = temperature
77
+ self.max_tokens = max_tokens
78
+ self.top_p = top_p
79
+ self.top_k = top_k
80
+ self.system_prompt = system_prompt
81
+ self.timeout = timeout
82
+ self.max_retries = max_retries
83
+ self._extra_kwargs = kwargs # Store extra params
84
+
85
+ client_kwargs: dict[str, Any] = {
86
+ "api_key": self.api_key,
87
+ "timeout": timeout,
88
+ "max_retries": max_retries,
89
+ }
90
+
91
+ if base_url := kwargs.get("base_url"):
92
+ client_kwargs["base_url"] = base_url
93
+
94
+ self.client = AsyncAnthropic(**client_kwargs)
95
+
96
+ async def aresponse(self, messages: MessageList) -> str | None:
97
+ """Generate a response using Anthropic's API.
98
+
99
+ Args
100
+ ----
101
+ messages: List of Message objects with role and content
102
+
103
+ Returns
104
+ -------
105
+ The generated response text, or None if failed
106
+ """
107
+ try:
108
+ # Anthropic requires system messages to be separate
109
+ system_message = self.system_prompt
110
+ anthropic_messages = []
111
+
112
+ for msg in messages:
113
+ if msg.role == "system":
114
+ # Concatenate multiple system messages if present
115
+ if system_message:
116
+ system_message += "\n" + msg.content
117
+ else:
118
+ system_message = msg.content
119
+ else:
120
+ anthropic_messages.append({"role": msg.role, "content": msg.content})
121
+
122
+ request_params: dict[str, Any] = {
123
+ "model": self.model,
124
+ "messages": anthropic_messages,
125
+ "temperature": self.temperature,
126
+ "max_tokens": self.max_tokens,
127
+ "top_p": self.top_p,
128
+ }
129
+
130
+ if system_message is not None:
131
+ request_params["system"] = system_message
132
+
133
+ if self.top_k is not None:
134
+ request_params["top_k"] = self.top_k
135
+
136
+ if stop_sequences := self._extra_kwargs.get("stop_sequences"):
137
+ request_params["stop_sequences"] = stop_sequences
138
+
139
+ response = await self.client.messages.create(**request_params)
140
+
141
+ if response.content and len(response.content) > 0:
142
+ first_content = response.content[0]
143
+ if hasattr(first_content, "text"):
144
+ return str(first_content.text)
145
+
146
+ logger.warning("No text content in Anthropic response")
147
+ return None
148
+
149
+ except Exception as e:
150
+ logger.error(f"Anthropic API error: {e}", exc_info=True)
151
+ return None
@@ -0,0 +1,6 @@
1
+ """Database adapters for HexDAG."""
2
+
3
+ from hexdag.builtin.adapters.database.pgvector.pgvector_adapter import PgVectorAdapter
4
+ from hexdag.builtin.adapters.database.sqlite.sqlite_adapter import SQLiteAdapter
5
+
6
+ __all__ = ["PgVectorAdapter", "SQLiteAdapter"]