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,269 @@
|
|
|
1
|
+
"""Port call node for direct adapter method invocation.
|
|
2
|
+
|
|
3
|
+
This node allows calling any method on a configured port directly from YAML,
|
|
4
|
+
eliminating Python wrapper functions for simple adapter operations.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
Basic usage in Python::
|
|
9
|
+
|
|
10
|
+
from hexdag.builtin.nodes import PortCallNode
|
|
11
|
+
|
|
12
|
+
node_factory = PortCallNode()
|
|
13
|
+
node = node_factory(
|
|
14
|
+
name="save_to_db",
|
|
15
|
+
port="database",
|
|
16
|
+
method="aexecute_query",
|
|
17
|
+
input_mapping={"query": "$input.sql_query"}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
YAML pipeline usage::
|
|
21
|
+
|
|
22
|
+
- kind: port_call_node
|
|
23
|
+
metadata:
|
|
24
|
+
name: execute_accept
|
|
25
|
+
spec:
|
|
26
|
+
port: database
|
|
27
|
+
method: record_acceptance
|
|
28
|
+
input_mapping:
|
|
29
|
+
load_id: $input.load_id
|
|
30
|
+
negotiation_id: get_context.negotiation.id
|
|
31
|
+
carrier_id: get_context.carrier.id
|
|
32
|
+
dependencies: [get_context]
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import asyncio
|
|
38
|
+
import time
|
|
39
|
+
from typing import TYPE_CHECKING, Any
|
|
40
|
+
|
|
41
|
+
from hexdag.core.context import get_port
|
|
42
|
+
from hexdag.core.logging import get_logger
|
|
43
|
+
|
|
44
|
+
from .base_node_factory import BaseNodeFactory
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from pydantic import BaseModel
|
|
48
|
+
|
|
49
|
+
from hexdag.core.domain.dag import NodeSpec
|
|
50
|
+
|
|
51
|
+
logger = get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PortCallNode(BaseNodeFactory):
|
|
55
|
+
"""Execute a method on a configured port/adapter.
|
|
56
|
+
|
|
57
|
+
This node type eliminates the need for Python wrapper functions
|
|
58
|
+
that simply extract fields from input and call adapter methods.
|
|
59
|
+
The port, method, and parameter mapping are defined declaratively
|
|
60
|
+
in the YAML configuration.
|
|
61
|
+
|
|
62
|
+
The node:
|
|
63
|
+
1. Resolves the port from the execution context
|
|
64
|
+
2. Uses input_mapping to prepare method arguments (handled by orchestrator)
|
|
65
|
+
3. Calls the method (supports both sync and async)
|
|
66
|
+
4. Returns the result with metadata
|
|
67
|
+
|
|
68
|
+
Parameters (in YAML spec)
|
|
69
|
+
-------------------------
|
|
70
|
+
port : str
|
|
71
|
+
Name of the port to use (e.g., "database", "llm", "cache")
|
|
72
|
+
method : str
|
|
73
|
+
Method name to call on the port (e.g., "aexecute_query", "aget")
|
|
74
|
+
input_mapping : dict[str, str], optional
|
|
75
|
+
Mapping of method parameter names to data sources.
|
|
76
|
+
Supports:
|
|
77
|
+
- ``$input.field`` - Extract from initial pipeline input
|
|
78
|
+
- ``dependency_name.field`` - Extract from a dependency's output
|
|
79
|
+
fallback : Any, optional
|
|
80
|
+
Value to return if the port is not available
|
|
81
|
+
has_fallback : bool, optional
|
|
82
|
+
Set to True to enable fallback behavior (allows None as fallback value)
|
|
83
|
+
|
|
84
|
+
Examples
|
|
85
|
+
--------
|
|
86
|
+
>>> factory = PortCallNode()
|
|
87
|
+
>>> node = factory(
|
|
88
|
+
... name="call_db",
|
|
89
|
+
... port="database",
|
|
90
|
+
... method="aexecute_query",
|
|
91
|
+
... )
|
|
92
|
+
>>> node.name
|
|
93
|
+
'call_db'
|
|
94
|
+
|
|
95
|
+
With input mapping::
|
|
96
|
+
|
|
97
|
+
>>> node = factory(
|
|
98
|
+
... name="record_data",
|
|
99
|
+
... port="database",
|
|
100
|
+
... method="record_acceptance",
|
|
101
|
+
... input_mapping={
|
|
102
|
+
... "load_id": "$input.load_id",
|
|
103
|
+
... "carrier_id": "context.carrier.id",
|
|
104
|
+
... },
|
|
105
|
+
... deps=["context"],
|
|
106
|
+
... )
|
|
107
|
+
>>> "context" in node.deps
|
|
108
|
+
True
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __call__(
|
|
112
|
+
self,
|
|
113
|
+
name: str,
|
|
114
|
+
port: str,
|
|
115
|
+
method: str,
|
|
116
|
+
input_mapping: dict[str, str] | None = None,
|
|
117
|
+
fallback: Any = None,
|
|
118
|
+
has_fallback: bool = False,
|
|
119
|
+
output_schema: dict[str, Any] | type[BaseModel] | None = None,
|
|
120
|
+
deps: list[str] | None = None,
|
|
121
|
+
**kwargs: Any,
|
|
122
|
+
) -> NodeSpec:
|
|
123
|
+
"""Create a NodeSpec for a port method invocation node.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
name : str
|
|
128
|
+
Node name (must be unique within the pipeline)
|
|
129
|
+
port : str
|
|
130
|
+
Name of the port to call (e.g., "database", "llm", "tool_router")
|
|
131
|
+
method : str
|
|
132
|
+
Method name to invoke on the port
|
|
133
|
+
input_mapping : dict[str, str] | None, optional
|
|
134
|
+
Mapping of method parameter names to data sources.
|
|
135
|
+
Supports ``$input.field`` and ``dependency_name.field`` syntax.
|
|
136
|
+
fallback : Any, optional
|
|
137
|
+
Value to return if the port is not available
|
|
138
|
+
has_fallback : bool, optional
|
|
139
|
+
Set to True to enable fallback behavior (allows None as fallback)
|
|
140
|
+
output_schema : dict[str, Any] | type[BaseModel] | None, optional
|
|
141
|
+
Optional schema for validating/structuring the output
|
|
142
|
+
deps : list[str] | None, optional
|
|
143
|
+
List of dependency node names for execution ordering
|
|
144
|
+
**kwargs : Any
|
|
145
|
+
Additional parameters stored in NodeSpec.params
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
NodeSpec
|
|
150
|
+
Complete node specification ready for execution
|
|
151
|
+
|
|
152
|
+
Examples
|
|
153
|
+
--------
|
|
154
|
+
>>> factory = PortCallNode()
|
|
155
|
+
>>> node = factory(
|
|
156
|
+
... name="save_to_db",
|
|
157
|
+
... port="database",
|
|
158
|
+
... method="aexecute_query",
|
|
159
|
+
... )
|
|
160
|
+
>>> node.name
|
|
161
|
+
'save_to_db'
|
|
162
|
+
"""
|
|
163
|
+
# Capture configuration in closure
|
|
164
|
+
port_name = port
|
|
165
|
+
method_name = method
|
|
166
|
+
_fallback = fallback
|
|
167
|
+
_has_fallback = has_fallback
|
|
168
|
+
|
|
169
|
+
async def port_call_fn(input_data: dict[str, Any]) -> dict[str, Any]:
|
|
170
|
+
"""Execute port method call."""
|
|
171
|
+
node_logger = logger.bind(
|
|
172
|
+
node=name,
|
|
173
|
+
node_type="port_call_node",
|
|
174
|
+
port=port_name,
|
|
175
|
+
method=method_name,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
start_time = time.perf_counter()
|
|
179
|
+
|
|
180
|
+
# Get the port from context
|
|
181
|
+
port_adapter = get_port(port_name)
|
|
182
|
+
|
|
183
|
+
if port_adapter is None:
|
|
184
|
+
if _has_fallback:
|
|
185
|
+
node_logger.warning(f"Port '{port_name}' not available, using fallback")
|
|
186
|
+
return {
|
|
187
|
+
"result": _fallback,
|
|
188
|
+
"port": port_name,
|
|
189
|
+
"method": method_name,
|
|
190
|
+
"error": f"Port '{port_name}' not available",
|
|
191
|
+
}
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
f"Port '{port_name}' not available in execution context. "
|
|
194
|
+
f"Ensure the port is configured in the orchestrator."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Verify method exists
|
|
198
|
+
if not hasattr(port_adapter, method_name):
|
|
199
|
+
available = [m for m in dir(port_adapter) if not m.startswith("_")]
|
|
200
|
+
raise AttributeError(
|
|
201
|
+
f"Port '{port_name}' has no method '{method_name}'. "
|
|
202
|
+
f"Available methods: {', '.join(available[:10])}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
method_fn = getattr(port_adapter, method_name)
|
|
206
|
+
|
|
207
|
+
# Prepare method arguments from input_data
|
|
208
|
+
# input_data is already processed by ExecutionCoordinator._apply_input_mapping
|
|
209
|
+
# if input_mapping was specified in the node params
|
|
210
|
+
method_kwargs = dict(input_data) if isinstance(input_data, dict) else {}
|
|
211
|
+
|
|
212
|
+
node_logger.info(
|
|
213
|
+
"Calling port method",
|
|
214
|
+
args=list(method_kwargs.keys()),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Call method (handle both sync and async)
|
|
219
|
+
if asyncio.iscoroutinefunction(method_fn):
|
|
220
|
+
result = await method_fn(**method_kwargs)
|
|
221
|
+
else:
|
|
222
|
+
result = method_fn(**method_kwargs)
|
|
223
|
+
|
|
224
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
225
|
+
node_logger.debug(
|
|
226
|
+
"Port method completed",
|
|
227
|
+
result_type=type(result).__name__,
|
|
228
|
+
duration_ms=f"{duration_ms:.2f}",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"result": result,
|
|
233
|
+
"port": port_name,
|
|
234
|
+
"method": method_name,
|
|
235
|
+
"error": None,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
node_logger.error(
|
|
240
|
+
"Port method failed",
|
|
241
|
+
error=str(e),
|
|
242
|
+
error_type=type(e).__name__,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if _has_fallback:
|
|
246
|
+
return {
|
|
247
|
+
"result": _fallback,
|
|
248
|
+
"port": port_name,
|
|
249
|
+
"method": method_name,
|
|
250
|
+
"error": str(e),
|
|
251
|
+
}
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
# Preserve function metadata for debugging
|
|
255
|
+
port_call_fn.__name__ = f"port_call_{name}"
|
|
256
|
+
port_call_fn.__doc__ = f"Port call: {port_name}.{method_name}"
|
|
257
|
+
|
|
258
|
+
# Build input schema from input_mapping if provided
|
|
259
|
+
input_schema = dict.fromkeys(input_mapping, Any) if input_mapping else None
|
|
260
|
+
|
|
261
|
+
return self.create_node_with_mapping(
|
|
262
|
+
name=name,
|
|
263
|
+
wrapped_fn=port_call_fn,
|
|
264
|
+
input_schema=input_schema,
|
|
265
|
+
output_schema=output_schema,
|
|
266
|
+
deps=deps,
|
|
267
|
+
input_mapping=input_mapping, # Pass to params for ExecutionCoordinator
|
|
268
|
+
**kwargs,
|
|
269
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""ToolCallNode - Execute a single tool call as a FunctionNode.
|
|
2
|
+
|
|
3
|
+
This node wraps a tool function and executes it as a node.
|
|
4
|
+
Used by ToolMacro to create parallel tool execution nodes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import inspect
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from hexdag.core.context import get_port
|
|
15
|
+
from hexdag.core.domain.dag import NodeSpec
|
|
16
|
+
from hexdag.core.logging import get_logger
|
|
17
|
+
from hexdag.core.orchestration.events import ToolCalled, ToolCompleted
|
|
18
|
+
from hexdag.core.resolver import resolve_function
|
|
19
|
+
|
|
20
|
+
from .base_node_factory import BaseNodeFactory
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolCallInput(BaseModel):
|
|
29
|
+
"""Input for a tool call."""
|
|
30
|
+
|
|
31
|
+
tool_name: str
|
|
32
|
+
arguments: dict[str, Any] = {}
|
|
33
|
+
tool_call_id: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ToolCallOutput(BaseModel):
|
|
37
|
+
"""Output from a tool call."""
|
|
38
|
+
|
|
39
|
+
result: Any
|
|
40
|
+
tool_call_id: str | None = None
|
|
41
|
+
tool_name: str
|
|
42
|
+
error: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ToolCallNode(BaseNodeFactory):
|
|
46
|
+
"""Execute a single tool call as a FunctionNode.
|
|
47
|
+
|
|
48
|
+
This node is a simple wrapper that:
|
|
49
|
+
1. Takes a tool name and arguments
|
|
50
|
+
2. Resolves the tool function using the module path resolver
|
|
51
|
+
3. Executes it and returns the result with metadata
|
|
52
|
+
4. Emits ToolCalled and ToolCompleted events
|
|
53
|
+
Used by ToolMacro to create parallel tool execution nodes.
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
Direct usage::
|
|
58
|
+
|
|
59
|
+
tool_node = ToolCallNode()(
|
|
60
|
+
name="search_tool",
|
|
61
|
+
tool_name="search",
|
|
62
|
+
arguments={"query": "python async"}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
result = await orchestrator.run(
|
|
66
|
+
graph,
|
|
67
|
+
{"tool_call_id": "1"}
|
|
68
|
+
)
|
|
69
|
+
# Returns: {"result": [...], "tool_call_id": "1", "tool_name": "search"}
|
|
70
|
+
|
|
71
|
+
Tool requiring ports::
|
|
72
|
+
|
|
73
|
+
# Tool definition
|
|
74
|
+
@tool(name="db_query", required_ports=["database"])
|
|
75
|
+
async def query_db(sql: str, database_port=None):
|
|
76
|
+
return await database_port.aexecute_query(sql)
|
|
77
|
+
|
|
78
|
+
# ToolCallNode automatically injects database port
|
|
79
|
+
db_tool = ToolCallNode()(
|
|
80
|
+
name="db_query_node",
|
|
81
|
+
tool_name="db_query",
|
|
82
|
+
arguments={"sql": "SELECT * FROM users"}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# When executed, database_port is injected from context
|
|
86
|
+
|
|
87
|
+
In a macro (automatic parallel execution)::
|
|
88
|
+
|
|
89
|
+
# ToolMacro creates multiple ToolCallNodes
|
|
90
|
+
tool1 = ToolCallNode()(name="tool_1", tool_name="search", ...)
|
|
91
|
+
tool2 = ToolCallNode()(name="tool_2", tool_name="calc", ...)
|
|
92
|
+
# Orchestrator executes them in parallel automatically
|
|
93
|
+
# Ports injected automatically for tools that need them
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
97
|
+
"""Initialize ToolCallNode."""
|
|
98
|
+
super().__init__()
|
|
99
|
+
|
|
100
|
+
def __call__(
|
|
101
|
+
self,
|
|
102
|
+
name: str,
|
|
103
|
+
tool_name: str,
|
|
104
|
+
arguments: dict[str, Any] | None = None,
|
|
105
|
+
tool_call_id: str | None = None,
|
|
106
|
+
deps: list[str] | None = None,
|
|
107
|
+
**kwargs: Any,
|
|
108
|
+
) -> NodeSpec:
|
|
109
|
+
"""Create a tool call execution node.
|
|
110
|
+
|
|
111
|
+
Args
|
|
112
|
+
----
|
|
113
|
+
name: Node name (should be unique)
|
|
114
|
+
tool_name: Full module path to the tool function (e.g., 'mymodule.my_tool')
|
|
115
|
+
arguments: Arguments to pass to the tool (default: {})
|
|
116
|
+
tool_call_id: Optional ID for tracking (from LLM tool calls)
|
|
117
|
+
deps: Dependencies (typically the LLM node that requested the tool)
|
|
118
|
+
**kwargs: Additional parameters
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
NodeSpec
|
|
123
|
+
Configured node specification for tool execution
|
|
124
|
+
"""
|
|
125
|
+
arguments = arguments or {}
|
|
126
|
+
|
|
127
|
+
async def execute_tool(input_data: dict[str, Any]) -> dict[str, Any]:
|
|
128
|
+
"""Execute the tool call with event emission."""
|
|
129
|
+
|
|
130
|
+
# Get observer for event emission (optional)
|
|
131
|
+
observer_manager = None
|
|
132
|
+
with contextlib.suppress(Exception):
|
|
133
|
+
observer_manager = get_port("observer_manager")
|
|
134
|
+
|
|
135
|
+
# Emit ToolCalled event
|
|
136
|
+
if observer_manager:
|
|
137
|
+
await observer_manager.notify(
|
|
138
|
+
ToolCalled(node_name=name, tool_name=tool_name, params=arguments)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
logger.debug(f"🔧 Executing tool '{tool_name}' with args: {arguments}")
|
|
142
|
+
|
|
143
|
+
start_time = time.time()
|
|
144
|
+
try:
|
|
145
|
+
# Resolve tool function using module path
|
|
146
|
+
tool_fn: Callable[..., Any] = resolve_function(tool_name)
|
|
147
|
+
|
|
148
|
+
# Prepare tool arguments
|
|
149
|
+
tool_kwargs = dict(arguments)
|
|
150
|
+
|
|
151
|
+
# Execute tool (handle both sync and async)
|
|
152
|
+
if inspect.iscoroutinefunction(tool_fn):
|
|
153
|
+
result = await tool_fn(**tool_kwargs)
|
|
154
|
+
else:
|
|
155
|
+
result = tool_fn(**tool_kwargs)
|
|
156
|
+
|
|
157
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
158
|
+
|
|
159
|
+
# Emit ToolCompleted event
|
|
160
|
+
if observer_manager:
|
|
161
|
+
await observer_manager.notify(
|
|
162
|
+
ToolCompleted(
|
|
163
|
+
node_name=name,
|
|
164
|
+
tool_name=tool_name,
|
|
165
|
+
result=result,
|
|
166
|
+
duration_ms=duration_ms,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
logger.debug(f"✅ Tool '{tool_name}' completed in {duration_ms:.2f}ms")
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"result": result,
|
|
174
|
+
"tool_call_id": tool_call_id,
|
|
175
|
+
"tool_name": tool_name,
|
|
176
|
+
"error": None,
|
|
177
|
+
}
|
|
178
|
+
except Exception as e:
|
|
179
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
180
|
+
logger.warning(f"❌ Tool '{tool_name}' failed after {duration_ms:.2f}ms: {e}")
|
|
181
|
+
return {
|
|
182
|
+
"result": None,
|
|
183
|
+
"tool_call_id": tool_call_id,
|
|
184
|
+
"tool_name": tool_name,
|
|
185
|
+
"error": str(e),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return self.create_node_with_mapping(
|
|
189
|
+
name=name,
|
|
190
|
+
wrapped_fn=execute_tool,
|
|
191
|
+
input_schema={}, # No specific input schema (uses context)
|
|
192
|
+
output_schema=ToolCallOutput,
|
|
193
|
+
deps=deps,
|
|
194
|
+
**kwargs,
|
|
195
|
+
)
|