hexdag 0.5.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,423 @@
1
+ """ReasoningAgentMacro - Multi-step reasoning with adaptive tool calling.
2
+
3
+ Architecture:
4
+ - **Native Tool Calling**: Uses LLM adapter's aresponse_with_tools for OpenAI/Anthropic/Gemini
5
+ - **Text-Based Fallback**: INVOKE_TOOL: parsing for adapters without native support
6
+ - **Runtime Detection**: Adapts automatically based on adapter capabilities
7
+ - **Fallback Policy Support**: Seamlessly handles adapter switching during failures
8
+
9
+ This adaptive approach ensures:
10
+ - Optimal performance with native tool calling when available
11
+ - Compatibility with any LLM via text-based fallback
12
+ - Seamless integration with hexDAG's fallback policies
13
+ - Single graph works with multiple adapter types
14
+
15
+ Example workflow:
16
+ 1. LLM reasoning step → tries native tools first, falls back to text if needed
17
+ 2. Tool executor → executes parsed tool calls
18
+ 3. Result merger → combines reasoning with tool results
19
+ 4. Next step → continues with context from previous tools
20
+ """
21
+
22
+ from typing import Any
23
+
24
+ from pydantic import field_validator
25
+
26
+ from hexdag.builtin.nodes.function_node import FunctionNode
27
+ from hexdag.builtin.nodes.llm_node import LLMNode
28
+ from hexdag.builtin.nodes.tool_utils import (
29
+ ToolCallFormat,
30
+ ToolDefinition,
31
+ ToolParser,
32
+ ToolSchemaConverter,
33
+ )
34
+ from hexdag.builtin.prompts.tool_prompts import get_tool_prompt_for_format
35
+ from hexdag.core.configurable import ConfigurableMacro, MacroConfig
36
+ from hexdag.core.context import get_port
37
+ from hexdag.core.domain.dag import DirectedGraph
38
+ from hexdag.core.logging import get_logger
39
+ from hexdag.core.orchestration.prompt import PromptTemplate
40
+ from hexdag.core.ports.llm import Message, MessageList
41
+ from hexdag.core.resolver import resolve_function
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ class ReasoningAgentConfig(MacroConfig):
47
+ """Configuration for ReasoningAgentMacro.
48
+
49
+ Attributes
50
+ ----------
51
+ main_prompt : str
52
+ Primary prompt for reasoning
53
+ max_steps : int
54
+ Maximum reasoning iterations (default: 3)
55
+ allowed_tools : list[str]
56
+ Tool names available to the agent (e.g., ["core:search", "core:calculate"])
57
+ Uses qualified names (namespace:name)
58
+ tool_format : ToolCallFormat
59
+ Tool calling format for text-based fallback: FUNCTION_CALL, JSON, or MIXED (default: MIXED)
60
+ Only used when adapter doesn't support native tool calling
61
+ """
62
+
63
+ main_prompt: str
64
+ max_steps: int = 5
65
+ allowed_tools: list[str] = []
66
+ tool_format: ToolCallFormat = ToolCallFormat.MIXED
67
+
68
+ @field_validator("main_prompt", mode="before")
69
+ @classmethod
70
+ def convert_prompt_input(cls, v: Any) -> str:
71
+ """Convert PromptInput to string."""
72
+ if isinstance(v, str):
73
+ return v
74
+ if isinstance(v, PromptTemplate):
75
+ return v.template
76
+ if hasattr(v, "template"):
77
+ return str(v.template)
78
+ return str(v)
79
+
80
+
81
+ class ReasoningAgentMacro(ConfigurableMacro):
82
+ """Multi-step reasoning agent with adaptive tool calling.
83
+
84
+ Automatically detects and adapts to LLM adapter capabilities:
85
+ - **Native mode**: OpenAI/Anthropic/Gemini → clean prompts, structured tool calls
86
+ - **Text mode**: Local/other LLMs → INVOKE_TOOL: parsing from text
87
+ - **Runtime adaptive**: Checks hasattr(llm, 'aresponse_with_tools') at execution time
88
+
89
+ This adaptive approach enables:
90
+ 1. **Fallback Policy Support**: When a node fails and policy switches adapters,
91
+ the same graph works with the new adapter type
92
+ 2. **Flexibility**: Single graph handles multiple adapter types
93
+ 3. **Optimal Performance**: Uses native tools when available
94
+
95
+ Architecture per reasoning step:
96
+ ```
97
+ LLM Node (adaptive) → Tool Executor → Result Merger
98
+ ```
99
+
100
+ The LLM node adapts at runtime:
101
+ - Tries native tool calling first (if available)
102
+ - Falls back to text-based parsing automatically
103
+ - Both paths prepared, chosen based on adapter capabilities
104
+ """
105
+
106
+ Config = ReasoningAgentConfig
107
+
108
+ def expand(
109
+ self,
110
+ instance_name: str,
111
+ inputs: dict[str, Any],
112
+ dependencies: list[str],
113
+ ) -> DirectedGraph:
114
+ """Expand into reasoning chain with adaptive tool calling strategy."""
115
+ graph = DirectedGraph()
116
+ config: ReasoningAgentConfig = self.config # type: ignore[assignment]
117
+
118
+ fn_factory = FunctionNode()
119
+
120
+ # Build tool schemas for native calling
121
+ tool_schemas = self._build_tool_schemas_for_native(config.allowed_tools)
122
+
123
+ # Build tool list for text-based fallback
124
+ tool_list_text = self._build_tool_list_for_text(config.allowed_tools)
125
+ tool_prompt = get_tool_prompt_for_format(config.tool_format)
126
+
127
+ # Build reasoning chain
128
+ prev_step: str | None = None
129
+ for step_idx in range(config.max_steps):
130
+ step_name_prefix = f"{instance_name}_step_{step_idx}"
131
+ step_deps: list[str] = dependencies if step_idx == 0 else [prev_step] # type: ignore[list-item]
132
+
133
+ # Create LLM subgraph (unified LLMNode + adapter)
134
+ llm_subgraph = self._create_llm_subgraph(
135
+ fn_factory,
136
+ step_name_prefix,
137
+ step_idx,
138
+ prev_step,
139
+ config,
140
+ tool_schemas,
141
+ tool_list_text,
142
+ tool_prompt,
143
+ step_deps,
144
+ )
145
+ graph |= llm_subgraph
146
+
147
+ # Create tool executor node
148
+ tool_executor = self._create_tool_executor_node(fn_factory, step_name_prefix, config)
149
+ graph += tool_executor
150
+
151
+ # Create result merger node
152
+ result_merger = self._create_result_merger_node(fn_factory, step_name_prefix)
153
+ graph += result_merger
154
+
155
+ prev_step = f"{step_name_prefix}_result_merger"
156
+
157
+ # Final consolidation using unified LLMNode
158
+ if prev_step is None:
159
+ raise ValueError("prev_step is None")
160
+
161
+ llm_factory = LLMNode()
162
+ final_llm = llm_factory(
163
+ name=f"{instance_name}_final",
164
+ prompt_template=f"""All reasoning steps and tool results:
165
+ {{{{{prev_step}}}}}
166
+
167
+ Provide your final conclusion based on all reasoning and evidence gathered.""",
168
+ deps=[prev_step],
169
+ )
170
+ graph += final_llm
171
+
172
+ return graph
173
+
174
+ def _create_llm_subgraph(
175
+ self,
176
+ fn_factory: FunctionNode,
177
+ step_name: str,
178
+ step_idx: int,
179
+ prev_step: str | None,
180
+ config: ReasoningAgentConfig,
181
+ tool_schemas: list[dict[str, Any]],
182
+ tool_list_text: str,
183
+ tool_prompt: Any,
184
+ deps: list[str],
185
+ ) -> DirectedGraph:
186
+ """Create LLM nodes using unified LLMNode architecture.
187
+
188
+ Returns a subgraph with:
189
+ - LLMNode: unified node for prompt + LLM call
190
+ - FunctionNode: adapts response and parses tool calls
191
+ """
192
+ subgraph = DirectedGraph()
193
+
194
+ # Build prompt content
195
+ if step_idx == 0:
196
+ base_prompt = config.main_prompt
197
+ else:
198
+ base_prompt = f"""Previous reasoning and tool results:
199
+ {{{{{prev_step}}}}}
200
+
201
+ Continue reasoning. Use tools if needed to gather more information."""
202
+
203
+ # Add tool instructions for text-based fallback
204
+ full_prompt = f"""{base_prompt}
205
+
206
+ ## Available Tools
207
+ {tool_list_text}
208
+
209
+ {tool_prompt.template if hasattr(tool_prompt, "template") else str(tool_prompt)}"""
210
+
211
+ # Node 1: Unified LLMNode for prompt + LLM call
212
+ llm_factory = LLMNode()
213
+ llm_node = llm_factory(
214
+ name=f"{step_name}_llm",
215
+ prompt_template=full_prompt,
216
+ deps=deps,
217
+ )
218
+ subgraph += llm_node
219
+
220
+ # Node 2: Adapter function to normalize response and parse tool calls
221
+ async def normalize_response(input_data: str, **kwargs: Any) -> dict[str, Any]:
222
+ """Normalize LLM response to unified format with tool parsing."""
223
+ text = input_data if isinstance(input_data, str) else str(input_data)
224
+
225
+ # Try native tool calling if LLM port supports it
226
+ llm_port = get_port("llm")
227
+ has_tool_support = (
228
+ llm_port and hasattr(llm_port, "aresponse_with_tools") and tool_schemas
229
+ )
230
+
231
+ if has_tool_support:
232
+ # Check if adapter actually implements aresponse_with_tools
233
+ for cls in llm_port.__class__.__mro__:
234
+ if (
235
+ cls.__name__ not in ["LLM", "Protocol", "object"]
236
+ and "aresponse_with_tools" in cls.__dict__
237
+ ):
238
+ # Native tool calling available but we already got text response
239
+ # Parse any tool calls from the text (in case it includes them)
240
+ break
241
+
242
+ # Text-based - parse tool calls from text
243
+ parsed_calls = ToolParser.parse_tool_calls(text, format=config.tool_format)
244
+
245
+ return {
246
+ "content": text,
247
+ "tool_calls": [
248
+ {"id": f"text_{i}", "name": tc.name, "arguments": tc.params}
249
+ for i, tc in enumerate(parsed_calls)
250
+ ],
251
+ "strategy": "text",
252
+ }
253
+
254
+ adapter_node = fn_factory(
255
+ name=f"{step_name}_adapter",
256
+ fn=normalize_response,
257
+ deps=[f"{step_name}_llm"],
258
+ )
259
+ subgraph += adapter_node
260
+
261
+ return subgraph
262
+
263
+ def _create_tool_executor_node(
264
+ self, fn_factory: FunctionNode, step_name: str, config: ReasoningAgentConfig
265
+ ) -> Any:
266
+ """Create node that executes tool calls (from native or parsed)."""
267
+
268
+ async def execute_tools(
269
+ input_data: Any, tool_router: Any = None, **kwargs: Any
270
+ ) -> dict[str, Any]:
271
+ """Execute tool calls and return results."""
272
+ llm_output = input_data
273
+ tool_calls = llm_output.get("tool_calls", [])
274
+
275
+ if not tool_calls:
276
+ return {
277
+ "llm_content": llm_output.get("content", ""),
278
+ "tool_results": [],
279
+ "has_tools": False,
280
+ }
281
+
282
+ # Build tool name mapping for allowed tools
283
+ tool_name_map = {tool: tool for tool in config.allowed_tools}
284
+
285
+ # Execute tools
286
+ tool_results = []
287
+ for tc in tool_calls:
288
+ try:
289
+ tool_name = tc["name"]
290
+ resolved_name = tool_name_map.get(tool_name) or tool_name
291
+
292
+ # Execute tool
293
+ if tool_router:
294
+ result = await tool_router.acall_tool(resolved_name, tc["arguments"])
295
+ else:
296
+ # Direct resolution via module path
297
+ import asyncio
298
+
299
+ tool = resolve_function(resolved_name)
300
+ if asyncio.iscoroutinefunction(tool):
301
+ result = await tool(**tc["arguments"])
302
+ else:
303
+ result = tool(**tc["arguments"])
304
+
305
+ tool_results.append({
306
+ "tool_name": tc["name"],
307
+ "arguments": tc["arguments"],
308
+ "result": result,
309
+ })
310
+ except Exception as e:
311
+ logger.error(f"Tool execution error for {tc['name']}: {e}")
312
+ tool_results.append({
313
+ "tool_name": tc["name"],
314
+ "arguments": tc["arguments"],
315
+ "error": str(e),
316
+ })
317
+
318
+ return {
319
+ "llm_content": llm_output.get("content", ""),
320
+ "tool_results": tool_results,
321
+ "has_tools": True,
322
+ }
323
+
324
+ return fn_factory(
325
+ name=f"{step_name}_tool_executor",
326
+ fn=execute_tools,
327
+ deps=[f"{step_name}_adapter"],
328
+ )
329
+
330
+ def _create_result_merger_node(self, fn_factory: FunctionNode, step_name: str) -> Any:
331
+ """Create node that merges LLM reasoning with tool results."""
332
+
333
+ async def merge_results(input_data: Any, **kwargs: Any) -> str:
334
+ """Combine LLM content with tool results into readable format."""
335
+ executor_output = input_data
336
+ llm_content: str = executor_output.get("llm_content", "")
337
+ tool_results = executor_output.get("tool_results", [])
338
+ has_tools = executor_output.get("has_tools", False)
339
+
340
+ if not has_tools:
341
+ return llm_content
342
+
343
+ # Format tool results
344
+ results_text = "\n\n## Tool Execution Results:\n"
345
+ for tr in tool_results:
346
+ if "error" in tr:
347
+ results_text += f"- {tr['tool_name']}: ERROR - {tr['error']}\n"
348
+ else:
349
+ results_text += f"- {tr['tool_name']}: {tr['result']}\n"
350
+
351
+ return f"{llm_content}{results_text}"
352
+
353
+ return fn_factory(
354
+ name=f"{step_name}_result_merger", fn=merge_results, deps=[f"{step_name}_tool_executor"]
355
+ )
356
+
357
+ def _create_final_consolidation_fn(self, prev_step: str) -> Any:
358
+ """Create function for final consolidation of all reasoning."""
359
+
360
+ async def consolidate(input_data: Any, llm: Any, **kwargs: Any) -> str:
361
+ """Consolidate all reasoning steps into final answer."""
362
+ all_reasoning = kwargs.get(prev_step, "")
363
+
364
+ messages = MessageList([
365
+ Message(
366
+ role="user",
367
+ content=f"""All reasoning steps and tool results:
368
+ {all_reasoning}
369
+
370
+ Provide your final conclusion based on all reasoning and evidence gathered.""",
371
+ )
372
+ ])
373
+
374
+ response = await llm.aresponse(messages)
375
+ return response or ""
376
+
377
+ return consolidate
378
+
379
+ def _build_tool_schemas_for_native(self, allowed_tools: list[str]) -> list[dict[str, Any]]:
380
+ """Build OpenAI-format tool schemas for native calling."""
381
+ schemas = []
382
+ for tool_name in allowed_tools:
383
+ try:
384
+ # Resolve tool function to get its docstring
385
+ tool_fn = resolve_function(tool_name)
386
+ description = tool_fn.__doc__ or f"Tool {tool_name}"
387
+ # Take first line of docstring
388
+ description = description.split("\n")[0].strip()
389
+
390
+ # Build ToolDefinition
391
+ tool_def = ToolDefinition(
392
+ name=tool_name,
393
+ simplified_description=description,
394
+ detailed_description=description,
395
+ parameters=[],
396
+ examples=[],
397
+ )
398
+
399
+ # Convert to OpenAI format
400
+ schema = ToolSchemaConverter.to_openai_schema(tool_def)
401
+ schemas.append(schema)
402
+ except Exception as e:
403
+ logger.warning(f"Could not build schema for tool {tool_name}: {e}")
404
+
405
+ return schemas
406
+
407
+ def _build_tool_list_for_text(self, allowed_tools: list[str]) -> str:
408
+ """Build text-format tool list for fallback mode."""
409
+ if not allowed_tools:
410
+ return "No tools available"
411
+
412
+ tool_lines = []
413
+ for tool_name in allowed_tools:
414
+ try:
415
+ tool_fn = resolve_function(tool_name)
416
+ description = tool_fn.__doc__ or "No description"
417
+ # Take first line of docstring
418
+ description = description.split("\n")[0].strip()
419
+ tool_lines.append(f" - {tool_name}: {description}")
420
+ except Exception:
421
+ tool_lines.append(f" - {tool_name}: Tool description unavailable")
422
+
423
+ return "\n".join(tool_lines)