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,696 @@
|
|
|
1
|
+
"""ReActAgentNode - Multi-step reasoning agent."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Any, NotRequired, TypedDict
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
from hexdag.builtin.adapters.unified_tool_router import UnifiedToolRouter
|
|
13
|
+
from hexdag.core.context import get_port, get_ports
|
|
14
|
+
from hexdag.core.domain.dag import NodeSpec
|
|
15
|
+
from hexdag.core.logging import get_logger
|
|
16
|
+
from hexdag.core.orchestration.prompt import PromptInput
|
|
17
|
+
from hexdag.core.orchestration.prompt.template import PromptTemplate
|
|
18
|
+
from hexdag.core.ports.tool_router import ToolRouter
|
|
19
|
+
from hexdag.core.protocols import to_dict
|
|
20
|
+
|
|
21
|
+
from .base_node_factory import BaseNodeFactory
|
|
22
|
+
from .llm_node import LLMNode
|
|
23
|
+
from .tool_utils import ToolCallFormat, ToolParser
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from types import MappingProxyType
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PhaseContext(TypedDict):
|
|
32
|
+
"""Context structure for phase transitions in agents.
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
previous_phase : str, optional
|
|
37
|
+
The phase the agent is transitioning from
|
|
38
|
+
reason : str, optional
|
|
39
|
+
Explanation for why the phase change is occurring
|
|
40
|
+
carried_data : dict[str, Any], optional
|
|
41
|
+
Data to carry forward from the previous phase
|
|
42
|
+
target_output : str, optional
|
|
43
|
+
Expected output format or goal for the new phase
|
|
44
|
+
iteration : int, optional
|
|
45
|
+
Current iteration number if in a loop or retry scenario
|
|
46
|
+
metadata : dict[str, Any], optional
|
|
47
|
+
Additional metadata about the phase transition
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
previous_phase: NotRequired[str]
|
|
51
|
+
reason: NotRequired[str]
|
|
52
|
+
carried_data: NotRequired[dict[str, Any]]
|
|
53
|
+
target_output: NotRequired[str]
|
|
54
|
+
iteration: NotRequired[int]
|
|
55
|
+
metadata: NotRequired[dict[str, Any]]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AgentState(BaseModel):
|
|
59
|
+
"""Pydantic model for agent state - provides type safety and validation."""
|
|
60
|
+
|
|
61
|
+
# Original input data (preserved)
|
|
62
|
+
input_data: dict[str, Any] = {}
|
|
63
|
+
|
|
64
|
+
# Agent reasoning state
|
|
65
|
+
reasoning_steps: list[str] = []
|
|
66
|
+
tool_results: list[str] = []
|
|
67
|
+
tools_used: list[str] = []
|
|
68
|
+
current_phase: str = "main"
|
|
69
|
+
phase_history: list[str] = ["main"]
|
|
70
|
+
phase_contexts: dict[str, PhaseContext] = {} # Store typed context for each phase
|
|
71
|
+
step: int = 0
|
|
72
|
+
response: str = ""
|
|
73
|
+
|
|
74
|
+
# Loop iteration tracking
|
|
75
|
+
loop_iteration: int = 0
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(extra="allow") # Allow additional fields from input mapping
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class AgentConfig:
|
|
82
|
+
"""Agent configuration for multi-step reasoning (legacy - kept for backward compatibility)."""
|
|
83
|
+
|
|
84
|
+
max_steps: int = 20
|
|
85
|
+
tool_call_style: ToolCallFormat = ToolCallFormat.MIXED
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Agent:
|
|
89
|
+
"""Configuration for Agent Node.
|
|
90
|
+
|
|
91
|
+
Attributes
|
|
92
|
+
----------
|
|
93
|
+
max_steps : int
|
|
94
|
+
Maximum number of reasoning steps (default: 20)
|
|
95
|
+
tool_call_style : ToolCallFormat
|
|
96
|
+
Format for tool calls - MIXED, FUNCTION_CALL, or JSON (default: MIXED)
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
max_steps: int = 20
|
|
100
|
+
tool_call_style: ToolCallFormat = ToolCallFormat.MIXED
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ReActAgentNode(BaseNodeFactory):
|
|
104
|
+
"""Multi-step reasoning agent.
|
|
105
|
+
|
|
106
|
+
This agent:
|
|
107
|
+
1. Uses loop control internally for iteration control
|
|
108
|
+
2. Implements single-step reasoning logic
|
|
109
|
+
3. Maintains clean agent interface for users
|
|
110
|
+
4. Leverages proven loop control patterns
|
|
111
|
+
5. Supports all agent features (tools, phases, events)
|
|
112
|
+
|
|
113
|
+
Architecture:
|
|
114
|
+
```
|
|
115
|
+
Agent(input) -> Loop -> SingleStep -> Loop -> SingleStep -> ... -> Output
|
|
116
|
+
```
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
120
|
+
"""Initialize with dependencies."""
|
|
121
|
+
self.llm_node = LLMNode()
|
|
122
|
+
self.tool_parser = ToolParser()
|
|
123
|
+
|
|
124
|
+
def __call__(
|
|
125
|
+
self,
|
|
126
|
+
name: str,
|
|
127
|
+
main_prompt: PromptInput,
|
|
128
|
+
continuation_prompts: dict[str, PromptInput] | None = None,
|
|
129
|
+
output_schema: dict[str, type] | type[BaseModel] | None = None,
|
|
130
|
+
config: AgentConfig | None = None,
|
|
131
|
+
deps: list[str] | None = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> NodeSpec:
|
|
134
|
+
"""Create a multi-step reasoning agent with internal loop control.
|
|
135
|
+
|
|
136
|
+
Args
|
|
137
|
+
----
|
|
138
|
+
name: Agent name
|
|
139
|
+
main_prompt: Initial reasoning prompt
|
|
140
|
+
continuation_prompts: Phase-specific prompts
|
|
141
|
+
output_schema: Custom output schema for tool_end results
|
|
142
|
+
config: Agent configuration
|
|
143
|
+
deps: Dependencies
|
|
144
|
+
**kwargs: Additional parameters
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
NodeSpec
|
|
149
|
+
A configured node specification for the agent
|
|
150
|
+
"""
|
|
151
|
+
config = config or AgentConfig()
|
|
152
|
+
|
|
153
|
+
# Infer input schema from prompt
|
|
154
|
+
input_schema = self._infer_input_schema(main_prompt)
|
|
155
|
+
|
|
156
|
+
input_model = self.create_pydantic_model(f"{name}Input", input_schema)
|
|
157
|
+
if input_model is None:
|
|
158
|
+
input_model = type(f"{name}Input", (BaseModel,), {"__annotations__": {"input": str}})
|
|
159
|
+
output_model = self.create_pydantic_model(f"{name}Output", output_schema) or type(
|
|
160
|
+
f"{name}Output", (BaseModel,), {"__annotations__": {"output": str}}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
agent_fn = self._create_agent_with_loop(
|
|
164
|
+
name, main_prompt, continuation_prompts or {}, output_model, config
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Use universal input mapping method
|
|
168
|
+
return self.create_node_with_mapping(
|
|
169
|
+
name=name,
|
|
170
|
+
wrapped_fn=agent_fn,
|
|
171
|
+
input_schema=input_schema,
|
|
172
|
+
output_schema=output_model,
|
|
173
|
+
deps=deps,
|
|
174
|
+
**kwargs,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _infer_input_schema(self, prompt: PromptInput) -> dict[str, Any]:
|
|
178
|
+
"""Infer input schema from prompt template.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
dict[str, Any]
|
|
183
|
+
Inferred input schema mapping
|
|
184
|
+
"""
|
|
185
|
+
# Use the shared implementation from BaseNodeFactory
|
|
186
|
+
# AgentNode doesn't filter special params, so pass None
|
|
187
|
+
return BaseNodeFactory.infer_input_schema_from_template(prompt, special_params=None)
|
|
188
|
+
|
|
189
|
+
def _get_current_prompt(
|
|
190
|
+
self,
|
|
191
|
+
main_prompt: PromptInput,
|
|
192
|
+
continuation_prompts: dict[str, PromptInput],
|
|
193
|
+
current_phase: str,
|
|
194
|
+
) -> PromptInput:
|
|
195
|
+
"""Get the appropriate prompt for the current phase.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
PromptInput
|
|
200
|
+
The prompt to use for the current phase
|
|
201
|
+
"""
|
|
202
|
+
if current_phase != "main" and current_phase in continuation_prompts:
|
|
203
|
+
return continuation_prompts[current_phase]
|
|
204
|
+
return main_prompt
|
|
205
|
+
|
|
206
|
+
def _create_agent_with_loop(
|
|
207
|
+
self,
|
|
208
|
+
name: str,
|
|
209
|
+
main_prompt: PromptInput,
|
|
210
|
+
continuation_prompts: dict[str, PromptInput],
|
|
211
|
+
output_model: type[BaseModel],
|
|
212
|
+
config: AgentConfig,
|
|
213
|
+
) -> Callable[..., Any]:
|
|
214
|
+
"""Create agent function with internal loop composition for multi-step iteration.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
Callable[..., Any]
|
|
219
|
+
Agent function with internal loop control
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
async def single_step_executor(input_data: Any) -> Any:
|
|
223
|
+
"""Execute single reasoning step."""
|
|
224
|
+
from hexdag.core.context import get_port
|
|
225
|
+
|
|
226
|
+
ports: MappingProxyType[str, Any] | dict[Any, Any] = get_ports() or {}
|
|
227
|
+
|
|
228
|
+
state = self._initialize_or_update_state(input_data)
|
|
229
|
+
|
|
230
|
+
# Execute single reasoning step
|
|
231
|
+
updated_state = await self._execute_single_step(
|
|
232
|
+
state, name, main_prompt, continuation_prompts, config, dict(ports)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
final_output = await self._check_for_final_output(
|
|
236
|
+
updated_state, output_model, get_port("event_manager")
|
|
237
|
+
)
|
|
238
|
+
if final_output is not None:
|
|
239
|
+
return final_output
|
|
240
|
+
|
|
241
|
+
# Return AgentState directly (Pydantic-first design)
|
|
242
|
+
return updated_state
|
|
243
|
+
|
|
244
|
+
# Define success condition using loop concepts
|
|
245
|
+
def success_condition(result: Any) -> bool:
|
|
246
|
+
"""Check if agent should stop iterating."""
|
|
247
|
+
# Stop if we got the final structured output (not AgentState)
|
|
248
|
+
if not isinstance(result, AgentState):
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
# Stop if we reached max steps
|
|
252
|
+
if result.step >= config.max_steps:
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
# Stop if tool_end was detected
|
|
256
|
+
return "tool_end" in result.response.lower()
|
|
257
|
+
|
|
258
|
+
async def agent_with_internal_loop(input_data: Any) -> Any:
|
|
259
|
+
"""Agent executor with internal loop control."""
|
|
260
|
+
node_logger = logger.bind(node=name, node_type="agent_node")
|
|
261
|
+
start_time = time.perf_counter()
|
|
262
|
+
|
|
263
|
+
# Log agent start
|
|
264
|
+
node_logger.info(
|
|
265
|
+
"Starting agent",
|
|
266
|
+
max_steps=config.max_steps,
|
|
267
|
+
tool_call_style=config.tool_call_style.value,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Start with initial input
|
|
271
|
+
current_result = input_data
|
|
272
|
+
|
|
273
|
+
# Run the loop until success condition is met or max iterations reached
|
|
274
|
+
for step_num in range(config.max_steps):
|
|
275
|
+
# Log step start
|
|
276
|
+
node_logger.debug(
|
|
277
|
+
"Agent step starting",
|
|
278
|
+
step=step_num + 1,
|
|
279
|
+
max_steps=config.max_steps,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Execute single step
|
|
283
|
+
step_result = await single_step_executor(current_result)
|
|
284
|
+
|
|
285
|
+
# If not AgentState, it's the final output
|
|
286
|
+
if not isinstance(step_result, AgentState):
|
|
287
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
288
|
+
node_logger.info(
|
|
289
|
+
"Agent completed with direct output",
|
|
290
|
+
total_steps=step_num + 1,
|
|
291
|
+
duration_ms=f"{duration_ms:.2f}",
|
|
292
|
+
output_type=type(step_result).__name__,
|
|
293
|
+
)
|
|
294
|
+
return step_result
|
|
295
|
+
|
|
296
|
+
# Log step completion with state info
|
|
297
|
+
node_logger.debug(
|
|
298
|
+
"Agent step completed",
|
|
299
|
+
step=step_num + 1,
|
|
300
|
+
phase=step_result.current_phase,
|
|
301
|
+
tools_used_count=len(step_result.tools_used),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Check success condition
|
|
305
|
+
if success_condition(step_result):
|
|
306
|
+
final_output = await self._check_for_final_output(
|
|
307
|
+
step_result,
|
|
308
|
+
output_model,
|
|
309
|
+
get_port("event_manager"),
|
|
310
|
+
)
|
|
311
|
+
if final_output is not None:
|
|
312
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
313
|
+
node_logger.info(
|
|
314
|
+
"Agent completed",
|
|
315
|
+
total_steps=step_num + 1,
|
|
316
|
+
tools_used=step_result.tools_used,
|
|
317
|
+
phases=step_result.phase_history,
|
|
318
|
+
duration_ms=f"{duration_ms:.2f}",
|
|
319
|
+
)
|
|
320
|
+
return final_output
|
|
321
|
+
return step_result
|
|
322
|
+
|
|
323
|
+
# Continue with next iteration (pass AgentState directly)
|
|
324
|
+
current_result = step_result
|
|
325
|
+
|
|
326
|
+
# If we reach here, max steps reached
|
|
327
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
328
|
+
node_logger.warning(
|
|
329
|
+
"Agent reached max steps",
|
|
330
|
+
max_steps=config.max_steps,
|
|
331
|
+
duration_ms=f"{duration_ms:.2f}",
|
|
332
|
+
)
|
|
333
|
+
return current_result
|
|
334
|
+
|
|
335
|
+
return agent_with_internal_loop
|
|
336
|
+
|
|
337
|
+
def _initialize_or_update_state(self, input_data: Any) -> AgentState:
|
|
338
|
+
"""Initialize new state or update existing state from loop iteration.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
AgentState
|
|
343
|
+
Initialized or updated agent state
|
|
344
|
+
"""
|
|
345
|
+
# Case 1: Already AgentState (from previous iteration) - return as-is
|
|
346
|
+
if isinstance(input_data, AgentState):
|
|
347
|
+
return input_data
|
|
348
|
+
|
|
349
|
+
# Case 2: Dict with AgentState fields (legacy/backward compatibility)
|
|
350
|
+
if isinstance(input_data, dict) and "reasoning_steps" in input_data:
|
|
351
|
+
state: AgentState = AgentState.model_validate(input_data)
|
|
352
|
+
return state
|
|
353
|
+
|
|
354
|
+
# Case 3: Fresh input (first iteration) - wrap in AgentState
|
|
355
|
+
try:
|
|
356
|
+
raw_input = to_dict(input_data)
|
|
357
|
+
except TypeError:
|
|
358
|
+
# Fallback for non-dict types
|
|
359
|
+
raw_input = {"input": str(input_data)}
|
|
360
|
+
|
|
361
|
+
return AgentState(input_data=raw_input)
|
|
362
|
+
|
|
363
|
+
def _enhance_prompt_with_tools(
|
|
364
|
+
self, prompt: PromptInput, tool_router: ToolRouter | None, config: AgentConfig
|
|
365
|
+
) -> PromptInput:
|
|
366
|
+
"""Add tool instructions to the prompt.
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
PromptInput
|
|
371
|
+
Enhanced prompt with tool instructions
|
|
372
|
+
"""
|
|
373
|
+
if not tool_router:
|
|
374
|
+
return prompt
|
|
375
|
+
|
|
376
|
+
if isinstance(prompt, str):
|
|
377
|
+
prompt = PromptTemplate(prompt)
|
|
378
|
+
|
|
379
|
+
tool_instructions = self._build_tool_instructions(tool_router, config)
|
|
380
|
+
|
|
381
|
+
# Use the template's enhance method
|
|
382
|
+
return prompt + tool_instructions
|
|
383
|
+
|
|
384
|
+
def _build_tool_instructions(self, tool_router: ToolRouter, config: AgentConfig) -> str:
|
|
385
|
+
"""Build tool usage instructions based on the configured format.
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
str
|
|
390
|
+
Tool usage instructions text
|
|
391
|
+
"""
|
|
392
|
+
tool_schemas = tool_router.get_all_tool_schemas()
|
|
393
|
+
if not tool_schemas:
|
|
394
|
+
return "\n## No tools available"
|
|
395
|
+
|
|
396
|
+
tool_list = []
|
|
397
|
+
for name, schema in tool_schemas.items():
|
|
398
|
+
params = ", ".join(p["name"] for p in schema.get("parameters", []))
|
|
399
|
+
tool_list.append(f"- {name}({params}): {schema.get('description', 'No description')}")
|
|
400
|
+
|
|
401
|
+
tools_text = "\n".join(tool_list)
|
|
402
|
+
|
|
403
|
+
# Generate format-specific usage guidelines
|
|
404
|
+
usage_guidelines = self._get_format_specific_guidelines(config.tool_call_style)
|
|
405
|
+
|
|
406
|
+
return f"""
|
|
407
|
+
## Available Tools
|
|
408
|
+
{tools_text}
|
|
409
|
+
|
|
410
|
+
## Usage Guidelines
|
|
411
|
+
{usage_guidelines}
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
def _get_format_specific_guidelines(self, format_style: ToolCallFormat) -> str:
|
|
415
|
+
"""Generate format-specific tool calling guidelines.
|
|
416
|
+
|
|
417
|
+
Returns
|
|
418
|
+
-------
|
|
419
|
+
str
|
|
420
|
+
Format-specific guidelines text
|
|
421
|
+
"""
|
|
422
|
+
if format_style == ToolCallFormat.FUNCTION_CALL:
|
|
423
|
+
return """- Call ONE tool at a time: INVOKE_TOOL: tool_name(param='value')
|
|
424
|
+
- For final answer and structured output: INVOKE_TOOL: tool_end(field1='value1', field2='value2')
|
|
425
|
+
- For phase change: INVOKE_TOOL: change_phase(phase='new_phase', reason='why changing',
|
|
426
|
+
carried_data={'key': 'value'})"""
|
|
427
|
+
|
|
428
|
+
if format_style == ToolCallFormat.JSON:
|
|
429
|
+
return (
|
|
430
|
+
"""- Call ONE tool at a time: INVOKE_TOOL: """
|
|
431
|
+
"""{"tool": "tool_name", "params": {"param": "value"}}\n"""
|
|
432
|
+
"""- For final answer and structured output: INVOKE_TOOL: """
|
|
433
|
+
"""{"tool": "tool_end", "params": {"field1": "value1", "field2": "value2"}}\n"""
|
|
434
|
+
"""- For phase change: INVOKE_TOOL: """
|
|
435
|
+
"""{"tool": "change_phase", "params": {"phase": "new_phase", "reason": "why",
|
|
436
|
+
"carried_data": {"key": "val"}}}"""
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# ToolCallFormat.MIXED
|
|
440
|
+
return """- Call ONE tool at a time using either format:
|
|
441
|
+
- Function style: INVOKE_TOOL: tool_name(param='value')
|
|
442
|
+
- JSON style: INVOKE_TOOL: {"tool": "tool_name", "params": {"param": "value"}}
|
|
443
|
+
- For final answer and structured output:
|
|
444
|
+
- Function: INVOKE_TOOL: tool_end(field1='value1', field2='value2')
|
|
445
|
+
- JSON: INVOKE_TOOL: {"tool": "tool_end", "params": {"field1": "value1", "field2": "value2"}}
|
|
446
|
+
- For phase change:
|
|
447
|
+
- Function: INVOKE_TOOL: change_phase(phase='new_phase', reason='why',
|
|
448
|
+
carried_data={'key': 'val'})
|
|
449
|
+
- JSON: INVOKE_TOOL: {"tool": "change_phase", "params": {"phase": "new_phase",
|
|
450
|
+
"reason": "why", "carried_data": {"key": "val"}}}"""
|
|
451
|
+
|
|
452
|
+
async def _get_llm_response(
|
|
453
|
+
self, prompt: PromptInput, llm_input: dict[str, Any], ports: dict[str, Any], node_name: str
|
|
454
|
+
) -> str:
|
|
455
|
+
"""Get response from LLM.
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
str
|
|
460
|
+
LLM response text
|
|
461
|
+
"""
|
|
462
|
+
# Ensure we have a proper template (not string)
|
|
463
|
+
if isinstance(prompt, str):
|
|
464
|
+
prompt = PromptTemplate(prompt)
|
|
465
|
+
|
|
466
|
+
llm_node_spec = self.llm_node.from_template(node_name, template=prompt)
|
|
467
|
+
|
|
468
|
+
# Execute LLM with the prepared input (no ports passed - uses ExecutionContext)
|
|
469
|
+
return await llm_node_spec.fn(llm_input) # type: ignore[no-any-return]
|
|
470
|
+
|
|
471
|
+
async def _execute_single_step(
|
|
472
|
+
self,
|
|
473
|
+
state: AgentState,
|
|
474
|
+
name: str,
|
|
475
|
+
main_prompt: PromptInput,
|
|
476
|
+
continuation_prompts: dict[str, PromptInput],
|
|
477
|
+
config: AgentConfig,
|
|
478
|
+
ports: dict[str, Any],
|
|
479
|
+
) -> AgentState:
|
|
480
|
+
"""Execute a single reasoning step.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
AgentState
|
|
485
|
+
Updated agent state after step execution
|
|
486
|
+
"""
|
|
487
|
+
event_manager = ports.get("event_manager")
|
|
488
|
+
tool_router = ports.get("tool_router", UnifiedToolRouter())
|
|
489
|
+
|
|
490
|
+
current_step = max(state.loop_iteration, state.step) + 1
|
|
491
|
+
node_step_name = f"{name}_step_{current_step}"
|
|
492
|
+
|
|
493
|
+
current_prompt = self._get_current_prompt(
|
|
494
|
+
main_prompt, continuation_prompts, state.current_phase
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Enhance prompt with tools
|
|
498
|
+
enhanced_prompt = self._enhance_prompt_with_tools(current_prompt, tool_router, config)
|
|
499
|
+
|
|
500
|
+
current_phase_context = state.phase_contexts.get(state.current_phase, {})
|
|
501
|
+
|
|
502
|
+
# Build LLM input - only convert to dict when needed for template
|
|
503
|
+
# Merge state fields with template-specific overrides
|
|
504
|
+
llm_input = {
|
|
505
|
+
**state.model_dump(), # Convert only once, at template boundary
|
|
506
|
+
**state.input_data,
|
|
507
|
+
"reasoning_so_far": "\n".join(state.reasoning_steps) or "Starting reasoning...",
|
|
508
|
+
"phase_context": current_phase_context,
|
|
509
|
+
"phase_reason": current_phase_context.get("reason", ""),
|
|
510
|
+
"phase_target": current_phase_context.get("target_output", ""),
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
response = await self._get_llm_response(enhanced_prompt, llm_input, ports, node_step_name)
|
|
514
|
+
|
|
515
|
+
# Process tools and phase changes
|
|
516
|
+
await self._process_tools_and_phases(
|
|
517
|
+
response, state, tool_router, continuation_prompts, config, event_manager
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
state.reasoning_steps.append(f"Step {current_step}: {response}")
|
|
521
|
+
state.response = response
|
|
522
|
+
state.step = current_step
|
|
523
|
+
|
|
524
|
+
return state
|
|
525
|
+
|
|
526
|
+
def _should_terminate(self, response: str) -> bool:
|
|
527
|
+
"""Check if agent should terminate.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
bool
|
|
532
|
+
True if agent should terminate execution
|
|
533
|
+
"""
|
|
534
|
+
return "tool_end" in response.lower() or "Tool_END" in response
|
|
535
|
+
|
|
536
|
+
async def _process_tools_and_phases(
|
|
537
|
+
self,
|
|
538
|
+
response: str,
|
|
539
|
+
state: AgentState,
|
|
540
|
+
tool_router: ToolRouter | None,
|
|
541
|
+
continuation_prompts: dict[str, PromptInput],
|
|
542
|
+
config: AgentConfig,
|
|
543
|
+
event_manager: Any,
|
|
544
|
+
) -> None:
|
|
545
|
+
"""Process tool calls and phase changes."""
|
|
546
|
+
if not tool_router:
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
# Parse tool calls
|
|
550
|
+
tool_calls = self.tool_parser.parse_tool_calls(response, format=config.tool_call_style)
|
|
551
|
+
|
|
552
|
+
if tool_calls:
|
|
553
|
+
logger.debug(
|
|
554
|
+
"Parsed tool calls",
|
|
555
|
+
tool_count=len(tool_calls),
|
|
556
|
+
tools=[tc.name for tc in tool_calls],
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
for tool_call in tool_calls:
|
|
560
|
+
try:
|
|
561
|
+
# Log tool execution
|
|
562
|
+
logger.debug(
|
|
563
|
+
"Executing tool",
|
|
564
|
+
tool_name=tool_call.name,
|
|
565
|
+
params_preview=str(tool_call.params)[:100],
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Execute tool
|
|
569
|
+
result = await tool_router.acall_tool(tool_call.name, tool_call.params)
|
|
570
|
+
|
|
571
|
+
state.tool_results.append(f"{tool_call.name}: {result}")
|
|
572
|
+
state.tools_used.append(tool_call.name)
|
|
573
|
+
|
|
574
|
+
if tool_call.name == "change_phase" and isinstance(result, dict):
|
|
575
|
+
new_phase = result.get("new_phase")
|
|
576
|
+
context = result.get("context", {})
|
|
577
|
+
|
|
578
|
+
if new_phase and new_phase in continuation_prompts:
|
|
579
|
+
old_phase = state.current_phase
|
|
580
|
+
|
|
581
|
+
if "previous_phase" not in context:
|
|
582
|
+
context["previous_phase"] = state.current_phase
|
|
583
|
+
|
|
584
|
+
state.phase_contexts[new_phase] = context
|
|
585
|
+
|
|
586
|
+
state.current_phase = new_phase
|
|
587
|
+
state.phase_history.append(new_phase)
|
|
588
|
+
|
|
589
|
+
# Log phase transition
|
|
590
|
+
logger.info(
|
|
591
|
+
"Phase transition",
|
|
592
|
+
from_phase=old_phase,
|
|
593
|
+
to_phase=new_phase,
|
|
594
|
+
reason=context.get("reason", ""),
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# If there's carried_data, merge it into state.input_data
|
|
598
|
+
if "carried_data" in context and isinstance(context["carried_data"], dict):
|
|
599
|
+
state.input_data.update(context["carried_data"])
|
|
600
|
+
|
|
601
|
+
except Exception as e:
|
|
602
|
+
error_msg = f"{tool_call.name}: Error - {e}"
|
|
603
|
+
state.tool_results.append(error_msg)
|
|
604
|
+
logger.warning(
|
|
605
|
+
"Tool execution failed",
|
|
606
|
+
tool_name=tool_call.name,
|
|
607
|
+
error=str(e),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def _parse_tool_end_result(self, tool_result: str) -> dict[str, Any] | None:
|
|
611
|
+
"""Parse tool_end result string into structured data.
|
|
612
|
+
|
|
613
|
+
Args
|
|
614
|
+
----
|
|
615
|
+
tool_result : str
|
|
616
|
+
Tool result string in format "tool_end: {data}"
|
|
617
|
+
|
|
618
|
+
Returns
|
|
619
|
+
-------
|
|
620
|
+
dict[str, Any] | None
|
|
621
|
+
Parsed data dictionary or None if parsing fails
|
|
622
|
+
"""
|
|
623
|
+
if not tool_result or not tool_result.startswith("tool_end:"):
|
|
624
|
+
return None
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
result_str = tool_result.split(":", 1)[1].strip()
|
|
628
|
+
result_data = ast.literal_eval(result_str)
|
|
629
|
+
|
|
630
|
+
if isinstance(result_data, dict):
|
|
631
|
+
return result_data
|
|
632
|
+
return None
|
|
633
|
+
except (json.JSONDecodeError, SyntaxError, ValueError, IndexError):
|
|
634
|
+
# Failed to parse - return None to skip this result
|
|
635
|
+
# IndexError: split failed (malformed tool_end output)
|
|
636
|
+
return None
|
|
637
|
+
|
|
638
|
+
async def _emit_agent_metadata(self, state: AgentState, event_manager: Any) -> None:
|
|
639
|
+
"""Emit agent metadata trace event.
|
|
640
|
+
|
|
641
|
+
Args
|
|
642
|
+
----
|
|
643
|
+
state : AgentState
|
|
644
|
+
Current agent state
|
|
645
|
+
event_manager : Any
|
|
646
|
+
Event manager instance
|
|
647
|
+
"""
|
|
648
|
+
if event_manager and hasattr(event_manager, "add_trace"):
|
|
649
|
+
await event_manager.add_trace(
|
|
650
|
+
"agent_metadata",
|
|
651
|
+
{
|
|
652
|
+
"reasoning_steps": state.reasoning_steps,
|
|
653
|
+
"tools_used": list(set(state.tools_used)),
|
|
654
|
+
"reasoning_phases": state.phase_history,
|
|
655
|
+
"total_steps": state.step,
|
|
656
|
+
},
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
async def _check_for_final_output(
|
|
660
|
+
self,
|
|
661
|
+
state: AgentState,
|
|
662
|
+
output_model: type[BaseModel],
|
|
663
|
+
event_manager: Any,
|
|
664
|
+
) -> Any | None:
|
|
665
|
+
"""Check if we have a final output from tool_end calls.
|
|
666
|
+
|
|
667
|
+
Returns
|
|
668
|
+
-------
|
|
669
|
+
Any | None
|
|
670
|
+
Final output model instance or None if not found
|
|
671
|
+
"""
|
|
672
|
+
# Check for tool_end calls with structured output
|
|
673
|
+
for tool_result in reversed(state.tool_results):
|
|
674
|
+
parsed_data = self._parse_tool_end_result(tool_result)
|
|
675
|
+
|
|
676
|
+
if parsed_data is not None:
|
|
677
|
+
try:
|
|
678
|
+
# Emit metadata before returning final result
|
|
679
|
+
await self._emit_agent_metadata(state, event_manager)
|
|
680
|
+
|
|
681
|
+
return output_model.model_validate(parsed_data)
|
|
682
|
+
|
|
683
|
+
except (ValueError, TypeError) as e:
|
|
684
|
+
# Validation failed - try next tool_end result
|
|
685
|
+
logger.debug(
|
|
686
|
+
"Failed to validate tool_end result",
|
|
687
|
+
output_model=output_model.__name__,
|
|
688
|
+
error=str(e),
|
|
689
|
+
)
|
|
690
|
+
continue # Skip this tool result and try the next one
|
|
691
|
+
|
|
692
|
+
return None
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# Backward compatibility alias
|
|
696
|
+
ReasoningAgentNode = ReActAgentNode
|