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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- hexdag_plugins/storage/vector/pgvector.py +502 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""ToolMacro - Expand tool calls into parallel ToolCallNodes.
|
|
2
|
+
|
|
3
|
+
This macro enables dynamic parallel tool execution:
|
|
4
|
+
1. Takes a list of tool_calls from LLM
|
|
5
|
+
2. Creates a ToolCallNode for each tool
|
|
6
|
+
3. Executes them in parallel (via DAG waves)
|
|
7
|
+
4. Merges results back into conversation
|
|
8
|
+
|
|
9
|
+
Used by ReasoningAgent and Macro Agent for dynamic tool injection.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from hexdag.builtin.nodes import FunctionNode, ToolCallNode
|
|
15
|
+
from hexdag.core.configurable import ConfigurableMacro, MacroConfig
|
|
16
|
+
from hexdag.core.domain.dag import DirectedGraph
|
|
17
|
+
from hexdag.core.logging import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolMacroConfig(MacroConfig):
|
|
23
|
+
"""Configuration for ToolMacro.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
tool_calls : list[dict]
|
|
28
|
+
List of tool calls from LLM in format:
|
|
29
|
+
[
|
|
30
|
+
{"id": "call_1", "name": "search", "arguments": {"query": "AI"}},
|
|
31
|
+
{"id": "call_2", "name": "calc", "arguments": {"expr": "2+2"}}
|
|
32
|
+
]
|
|
33
|
+
agent_name : str | None
|
|
34
|
+
Name of agent requesting tools (for access control)
|
|
35
|
+
allowed_tools : list[str] | None
|
|
36
|
+
List of allowed tool names (if None, all tools allowed)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
tool_calls: list[dict[str, Any]] = []
|
|
40
|
+
agent_name: str | None = None
|
|
41
|
+
allowed_tools: list[str] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ToolMacro(ConfigurableMacro):
|
|
45
|
+
"""Expand tool calls into parallel ToolCallNodes.
|
|
46
|
+
|
|
47
|
+
This macro creates a subgraph for dynamic parallel tool execution:
|
|
48
|
+
|
|
49
|
+
Graph Structure:
|
|
50
|
+
----------------
|
|
51
|
+
```
|
|
52
|
+
[dependencies] → tool_call_1 ┐
|
|
53
|
+
→ tool_call_2 ├─→ merger → [output]
|
|
54
|
+
→ tool_call_3 ┘
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All tool nodes execute in parallel (same wave), then results merge.
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
Basic usage::
|
|
62
|
+
|
|
63
|
+
config = ToolMacroConfig(
|
|
64
|
+
tool_calls=[
|
|
65
|
+
{"id": "1", "name": "search", "arguments": {"query": "AI"}},
|
|
66
|
+
{"id": "2", "name": "calc", "arguments": {"expression": "2+2"}}
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
macro = ToolMacro(config)
|
|
71
|
+
graph = macro.expand(
|
|
72
|
+
instance_name="agent_tools",
|
|
73
|
+
inputs={},
|
|
74
|
+
dependencies=["llm_node"]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Graph contains:
|
|
78
|
+
# - agent_tools_tool_0_search (depends on llm_node)
|
|
79
|
+
# - agent_tools_tool_1_calc (depends on llm_node)
|
|
80
|
+
# - agent_tools_merger (depends on both tools)
|
|
81
|
+
|
|
82
|
+
With access control::
|
|
83
|
+
|
|
84
|
+
config = ToolMacroConfig(
|
|
85
|
+
tool_calls=[...],
|
|
86
|
+
agent_name="research_agent",
|
|
87
|
+
allowed_tools=["search", "summarize"] # calc not allowed
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
macro = ToolMacro(config)
|
|
91
|
+
graph = macro.expand(...)
|
|
92
|
+
# Only creates nodes for allowed tools
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
Config = ToolMacroConfig
|
|
96
|
+
|
|
97
|
+
def expand(
|
|
98
|
+
self,
|
|
99
|
+
instance_name: str,
|
|
100
|
+
inputs: dict[str, Any],
|
|
101
|
+
dependencies: list[str],
|
|
102
|
+
) -> DirectedGraph:
|
|
103
|
+
"""Expand tool calls into parallel execution graph.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
instance_name : str
|
|
108
|
+
Unique name for this macro instance (used as prefix for nodes)
|
|
109
|
+
inputs : dict[str, Any]
|
|
110
|
+
Input values (typically empty, config has the data)
|
|
111
|
+
dependencies : list[str]
|
|
112
|
+
Nodes that tool calls depend on (typically the LLM node)
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
DirectedGraph
|
|
117
|
+
Graph with parallel ToolCallNodes and merger
|
|
118
|
+
|
|
119
|
+
Examples
|
|
120
|
+
--------
|
|
121
|
+
Called during dynamic execution::
|
|
122
|
+
|
|
123
|
+
# Agent's LLM returns tool_calls
|
|
124
|
+
tool_calls = [
|
|
125
|
+
{"id": "1", "name": "search", "arguments": {"query": "AI"}},
|
|
126
|
+
{"id": "2", "name": "calc", "arguments": {"expr": "2+2"}}
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Create macro config
|
|
130
|
+
config = ToolMacroConfig(tool_calls=tool_calls)
|
|
131
|
+
macro = ToolMacro(config)
|
|
132
|
+
|
|
133
|
+
# Expand into graph
|
|
134
|
+
subgraph = macro.expand(
|
|
135
|
+
instance_name="agent_step_1_tools",
|
|
136
|
+
inputs={},
|
|
137
|
+
dependencies=["agent_step_1_llm"]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Orchestrator merges subgraph into main graph
|
|
141
|
+
# and executes tools in parallel
|
|
142
|
+
"""
|
|
143
|
+
graph = DirectedGraph()
|
|
144
|
+
config: ToolMacroConfig = self.config # type: ignore[assignment]
|
|
145
|
+
|
|
146
|
+
# Get tool calls from config
|
|
147
|
+
tool_calls = config.tool_calls
|
|
148
|
+
|
|
149
|
+
if not tool_calls:
|
|
150
|
+
logger.debug(f"No tool calls for {instance_name}, returning empty graph")
|
|
151
|
+
return self._create_passthrough(instance_name, dependencies)
|
|
152
|
+
|
|
153
|
+
# Filter by allowed tools (access control)
|
|
154
|
+
allowed_tools = self._get_allowed_tools(config)
|
|
155
|
+
filtered_calls = self._filter_tool_calls(tool_calls, allowed_tools, config.agent_name)
|
|
156
|
+
|
|
157
|
+
if not filtered_calls:
|
|
158
|
+
logger.warning(f"All {len(tool_calls)} tool calls filtered out for {config.agent_name}")
|
|
159
|
+
return self._create_passthrough(instance_name, dependencies)
|
|
160
|
+
|
|
161
|
+
# Create ToolCallNode for each tool call
|
|
162
|
+
tool_call_factory = ToolCallNode()
|
|
163
|
+
tool_nodes = []
|
|
164
|
+
|
|
165
|
+
for i, tc in enumerate(filtered_calls):
|
|
166
|
+
tool_name = tc["name"]
|
|
167
|
+
tool_call_id = tc.get("id", f"{instance_name}_{i}")
|
|
168
|
+
arguments = tc.get("arguments", {})
|
|
169
|
+
|
|
170
|
+
# Create node
|
|
171
|
+
node = tool_call_factory(
|
|
172
|
+
name=f"{instance_name}_tool_{i}_{tool_name}",
|
|
173
|
+
tool_name=tool_name,
|
|
174
|
+
arguments=arguments,
|
|
175
|
+
tool_call_id=tool_call_id,
|
|
176
|
+
deps=dependencies, # All tools depend on LLM output
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
graph += node
|
|
180
|
+
tool_nodes.append(node.name)
|
|
181
|
+
|
|
182
|
+
logger.debug(
|
|
183
|
+
f"Created tool node: {node.name} for tool '{tool_name}' with args {arguments}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Create merger node that consolidates all tool results
|
|
187
|
+
merger = self._create_merger_node(instance_name, tool_nodes, filtered_calls)
|
|
188
|
+
graph += merger
|
|
189
|
+
|
|
190
|
+
logger.info(
|
|
191
|
+
f"ToolMacro '{instance_name}' expanded to {len(tool_nodes)} "
|
|
192
|
+
f"parallel tool nodes + merger"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return graph
|
|
196
|
+
|
|
197
|
+
def _get_allowed_tools(self, config: ToolMacroConfig) -> set[str] | None:
|
|
198
|
+
"""Get set of allowed tools (None = all allowed).
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
config : ToolMacroConfig
|
|
203
|
+
Macro configuration
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
set[str] | None
|
|
208
|
+
Set of allowed tool names, or None if all allowed
|
|
209
|
+
"""
|
|
210
|
+
if config.allowed_tools is None:
|
|
211
|
+
return None # All tools allowed
|
|
212
|
+
|
|
213
|
+
return set(config.allowed_tools)
|
|
214
|
+
|
|
215
|
+
def _filter_tool_calls(
|
|
216
|
+
self,
|
|
217
|
+
tool_calls: list[dict[str, Any]],
|
|
218
|
+
allowed_tools: set[str] | None,
|
|
219
|
+
agent_name: str | None,
|
|
220
|
+
) -> list[dict[str, Any]]:
|
|
221
|
+
"""Filter tool calls by allowed tools (access control).
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
tool_calls : list[dict]
|
|
226
|
+
Raw tool calls from LLM
|
|
227
|
+
allowed_tools : set[str] | None
|
|
228
|
+
Set of allowed tool names (None = all allowed)
|
|
229
|
+
agent_name : str | None
|
|
230
|
+
Name of agent (for logging)
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
list[dict]
|
|
235
|
+
Filtered tool calls
|
|
236
|
+
"""
|
|
237
|
+
if allowed_tools is None:
|
|
238
|
+
return tool_calls # No filtering
|
|
239
|
+
|
|
240
|
+
filtered = []
|
|
241
|
+
for tc in tool_calls:
|
|
242
|
+
tool_name = tc["name"]
|
|
243
|
+
if tool_name in allowed_tools:
|
|
244
|
+
filtered.append(tc)
|
|
245
|
+
else:
|
|
246
|
+
logger.warning(
|
|
247
|
+
f"Tool '{tool_name}' not allowed for agent '{agent_name}' "
|
|
248
|
+
f"(allowed: {allowed_tools})"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return filtered
|
|
252
|
+
|
|
253
|
+
def _create_merger_node(
|
|
254
|
+
self,
|
|
255
|
+
instance_name: str,
|
|
256
|
+
tool_nodes: list[str],
|
|
257
|
+
tool_calls: list[dict[str, Any]],
|
|
258
|
+
) -> Any: # Returns NodeSpec
|
|
259
|
+
"""Create merger node that consolidates tool results.
|
|
260
|
+
|
|
261
|
+
The merger node:
|
|
262
|
+
1. Waits for all tool nodes to complete
|
|
263
|
+
2. Collects results
|
|
264
|
+
3. Formats them for the agent (tool messages)
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
instance_name : str
|
|
269
|
+
Macro instance name
|
|
270
|
+
tool_nodes : list[str]
|
|
271
|
+
Names of tool nodes to wait for
|
|
272
|
+
tool_calls : list[dict]
|
|
273
|
+
Original tool calls (for matching)
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
NodeSpec
|
|
278
|
+
Merger node specification
|
|
279
|
+
"""
|
|
280
|
+
fn_factory = FunctionNode()
|
|
281
|
+
|
|
282
|
+
async def merge_tool_results(input_data: dict[str, Any]) -> dict[str, Any]:
|
|
283
|
+
"""Merge results from all tool nodes.
|
|
284
|
+
|
|
285
|
+
This function receives the outputs of all tool nodes and
|
|
286
|
+
consolidates them into a format suitable for continuing
|
|
287
|
+
the conversation with the LLM.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
input_data : dict
|
|
292
|
+
Contains results from all dependency nodes
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
dict
|
|
297
|
+
Merged results with:
|
|
298
|
+
- results: List of tool results
|
|
299
|
+
- has_tools: True
|
|
300
|
+
- tool_messages: Formatted for LLM conversation
|
|
301
|
+
"""
|
|
302
|
+
results = []
|
|
303
|
+
|
|
304
|
+
# Collect results from each tool node
|
|
305
|
+
for node_name in tool_nodes:
|
|
306
|
+
# Get result from this tool node
|
|
307
|
+
tool_result = input_data.get(node_name)
|
|
308
|
+
|
|
309
|
+
if tool_result:
|
|
310
|
+
# ToolCallNode returns ToolCallOutput (Pydantic model)
|
|
311
|
+
if hasattr(tool_result, "model_dump"):
|
|
312
|
+
tool_result = tool_result.model_dump()
|
|
313
|
+
|
|
314
|
+
results.append(tool_result)
|
|
315
|
+
|
|
316
|
+
# Format as tool messages for LLM
|
|
317
|
+
tool_messages = [
|
|
318
|
+
{
|
|
319
|
+
"role": "tool",
|
|
320
|
+
"tool_call_id": result.get("tool_call_id"),
|
|
321
|
+
"name": result.get("tool_name"),
|
|
322
|
+
"content": str(result.get("result") or result.get("error", "Unknown error")),
|
|
323
|
+
}
|
|
324
|
+
for result in results
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
logger.debug(f"Merged {len(results)} tool results into {len(tool_messages)} messages")
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"results": results,
|
|
331
|
+
"has_tools": True,
|
|
332
|
+
"tool_messages": tool_messages,
|
|
333
|
+
"tool_count": len(results),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return fn_factory(
|
|
337
|
+
name=f"{instance_name}_merger",
|
|
338
|
+
fn=merge_tool_results,
|
|
339
|
+
deps=tool_nodes, # Wait for all tools
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _create_passthrough(self, instance_name: str, dependencies: list[str]) -> DirectedGraph:
|
|
343
|
+
"""Create passthrough node when no tools to execute.
|
|
344
|
+
|
|
345
|
+
Used when:
|
|
346
|
+
- No tool calls provided
|
|
347
|
+
- All tool calls filtered out by access control
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
instance_name : str
|
|
352
|
+
Macro instance name
|
|
353
|
+
dependencies : list[str]
|
|
354
|
+
Nodes to depend on
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
DirectedGraph
|
|
359
|
+
Graph with single passthrough node
|
|
360
|
+
"""
|
|
361
|
+
graph = DirectedGraph()
|
|
362
|
+
fn_factory = FunctionNode()
|
|
363
|
+
|
|
364
|
+
async def passthrough(input_data: dict[str, Any]) -> dict[str, Any]:
|
|
365
|
+
"""Passthrough when no tools."""
|
|
366
|
+
return {
|
|
367
|
+
"results": [],
|
|
368
|
+
"has_tools": False,
|
|
369
|
+
"tool_messages": [],
|
|
370
|
+
"tool_count": 0,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
passthrough_node = fn_factory(
|
|
374
|
+
name=f"{instance_name}_no_tools",
|
|
375
|
+
fn=passthrough,
|
|
376
|
+
deps=dependencies,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
graph += passthrough_node
|
|
380
|
+
return graph
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Node factories for the hexdag framework.
|
|
2
|
+
|
|
3
|
+
All BaseNodeFactory subclasses in this package are auto-discovered
|
|
4
|
+
and registered for YAML pipeline validation. Adding a new node only
|
|
5
|
+
requires creating the node file - no manual registration needed.
|
|
6
|
+
|
|
7
|
+
See hexdag.builtin.nodes._discovery for the auto-discovery mechanism.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .agent_node import ReActAgentNode
|
|
11
|
+
from .composite_node import CompositeNode
|
|
12
|
+
from .data_node import DataNode
|
|
13
|
+
from .expression_node import ExpressionNode
|
|
14
|
+
from .function_node import FunctionNode
|
|
15
|
+
from .llm_node import LLMNode
|
|
16
|
+
from .loop_node import ConditionalNode, LoopNode
|
|
17
|
+
from .port_call_node import PortCallNode
|
|
18
|
+
from .tool_call_node import ToolCallNode
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CompositeNode",
|
|
22
|
+
"ConditionalNode",
|
|
23
|
+
"DataNode",
|
|
24
|
+
"ExpressionNode",
|
|
25
|
+
"FunctionNode",
|
|
26
|
+
"LLMNode",
|
|
27
|
+
"LoopNode",
|
|
28
|
+
"PortCallNode",
|
|
29
|
+
"ReActAgentNode",
|
|
30
|
+
"ToolCallNode",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Bootstrap: Register auto-discovered node aliases with core resolver
|
|
34
|
+
# This maintains hexagonal architecture - builtin calls into core, not vice versa
|
|
35
|
+
from hexdag.builtin.nodes._discovery import discover_node_factories
|
|
36
|
+
from hexdag.core.resolver import register_builtin_aliases
|
|
37
|
+
|
|
38
|
+
register_builtin_aliases(discover_node_factories())
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Auto-discovery of node factories in builtin.nodes package.
|
|
2
|
+
|
|
3
|
+
This module provides automatic discovery of all BaseNodeFactory subclasses,
|
|
4
|
+
eliminating the need to manually register node types in multiple places.
|
|
5
|
+
|
|
6
|
+
Usage
|
|
7
|
+
-----
|
|
8
|
+
Adding a new node only requires creating the node file:
|
|
9
|
+
|
|
10
|
+
# hexdag/builtin/nodes/my_node.py
|
|
11
|
+
class MyNode(BaseNodeFactory):
|
|
12
|
+
def __call__(self, name: str, **kwargs) -> NodeSpec:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
The node is automatically available in YAML as:
|
|
16
|
+
- my_node
|
|
17
|
+
- core:my_node
|
|
18
|
+
- core:my
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import importlib
|
|
24
|
+
import pkgutil
|
|
25
|
+
import re
|
|
26
|
+
from functools import lru_cache
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _to_snake_case(name: str) -> str:
|
|
30
|
+
"""Convert CamelCase to snake_case.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> _to_snake_case("LLMNode")
|
|
35
|
+
'llm_node'
|
|
36
|
+
>>> _to_snake_case("ReActAgentNode")
|
|
37
|
+
're_act_agent_node'
|
|
38
|
+
>>> _to_snake_case("DataNode")
|
|
39
|
+
'data_node'
|
|
40
|
+
"""
|
|
41
|
+
# Handle acronyms like LLM, ReAct, then standard CamelCase
|
|
42
|
+
name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
|
|
43
|
+
name = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", name)
|
|
44
|
+
return name.lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@lru_cache(maxsize=1)
|
|
48
|
+
def discover_node_factories() -> dict[str, str]:
|
|
49
|
+
"""Discover all BaseNodeFactory subclasses and generate aliases.
|
|
50
|
+
|
|
51
|
+
Returns mapping: {alias: full_module_path}
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> aliases = discover_node_factories()
|
|
56
|
+
>>> "llm_node" in aliases
|
|
57
|
+
True
|
|
58
|
+
>>> "core:llm_node" in aliases
|
|
59
|
+
True
|
|
60
|
+
>>> "core:llm" in aliases
|
|
61
|
+
True
|
|
62
|
+
"""
|
|
63
|
+
# Import here to avoid circular imports
|
|
64
|
+
from hexdag.builtin.nodes.base_node_factory import BaseNodeFactory
|
|
65
|
+
|
|
66
|
+
aliases: dict[str, str] = {}
|
|
67
|
+
package = importlib.import_module("hexdag.builtin.nodes")
|
|
68
|
+
|
|
69
|
+
for module_info in pkgutil.iter_modules(package.__path__):
|
|
70
|
+
if module_info.name.startswith("_"):
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
module = importlib.import_module(f"hexdag.builtin.nodes.{module_info.name}")
|
|
75
|
+
except ImportError:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
for attr_name in dir(module):
|
|
79
|
+
if attr_name.startswith("_"):
|
|
80
|
+
continue
|
|
81
|
+
attr = getattr(module, attr_name)
|
|
82
|
+
if (
|
|
83
|
+
isinstance(attr, type)
|
|
84
|
+
and issubclass(attr, BaseNodeFactory)
|
|
85
|
+
and attr is not BaseNodeFactory
|
|
86
|
+
):
|
|
87
|
+
full_path = f"hexdag.builtin.nodes.{attr_name}"
|
|
88
|
+
snake_name = _to_snake_case(attr_name)
|
|
89
|
+
|
|
90
|
+
# Generate all alias forms
|
|
91
|
+
aliases[snake_name] = full_path # llm_node
|
|
92
|
+
aliases[f"core:{snake_name}"] = full_path # core:llm_node
|
|
93
|
+
|
|
94
|
+
# Also add without _node suffix for convenience
|
|
95
|
+
if snake_name.endswith("_node"):
|
|
96
|
+
base = snake_name[:-5]
|
|
97
|
+
aliases[f"core:{base}"] = full_path # core:llm
|
|
98
|
+
|
|
99
|
+
# Add backwards compatibility aliases for legacy names
|
|
100
|
+
# static_node -> DataNode
|
|
101
|
+
if "data_node" in aliases:
|
|
102
|
+
aliases["static_node"] = aliases["data_node"]
|
|
103
|
+
aliases["core:static_node"] = aliases["data_node"]
|
|
104
|
+
aliases["core:static"] = aliases["data_node"]
|
|
105
|
+
|
|
106
|
+
# agent_node -> ReActAgentNode (legacy alias)
|
|
107
|
+
if "re_act_agent_node" in aliases:
|
|
108
|
+
aliases["agent_node"] = aliases["re_act_agent_node"]
|
|
109
|
+
aliases["core:agent_node"] = aliases["re_act_agent_node"]
|
|
110
|
+
aliases["core:agent"] = aliases["re_act_agent_node"]
|
|
111
|
+
|
|
112
|
+
return aliases
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_known_node_types() -> frozenset[str]:
|
|
116
|
+
"""Get all valid node type names for YAML validation.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
frozenset[str]
|
|
121
|
+
Set of all valid node type aliases
|
|
122
|
+
"""
|
|
123
|
+
return frozenset(discover_node_factories().keys())
|