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,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)
|