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,177 @@
|
|
|
1
|
+
"""Mock LLM implementation for testing purposes."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from hexdag.core.ports.llm import LLM, MessageList
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from hexdag.core.ports.healthcheck import HealthStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MockLLM(LLM):
|
|
13
|
+
"""Mock implementation of the LLM interface for testing.
|
|
14
|
+
|
|
15
|
+
The LLM port interface is stateless, but this mock provides testing utilities like response
|
|
16
|
+
sequencing and call inspection without violating the port contract.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Type annotations for attributes
|
|
20
|
+
delay_seconds: float
|
|
21
|
+
responses: list[str]
|
|
22
|
+
call_count: int
|
|
23
|
+
last_messages: MessageList | None
|
|
24
|
+
should_raise: bool
|
|
25
|
+
mock_tool_calls: list[dict[str, Any] | list[dict[str, Any]]] | None
|
|
26
|
+
|
|
27
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
28
|
+
"""Initialize with configuration.
|
|
29
|
+
|
|
30
|
+
Args
|
|
31
|
+
----
|
|
32
|
+
**kwargs: Configuration options
|
|
33
|
+
- responses: List of text responses or single response string
|
|
34
|
+
- delay_seconds: Delay before returning responses
|
|
35
|
+
- mock_tool_calls: List of tool call configurations for testing
|
|
36
|
+
"""
|
|
37
|
+
self.delay_seconds = kwargs.get("delay_seconds", 0.0)
|
|
38
|
+
|
|
39
|
+
# Process responses (convert to list if needed)
|
|
40
|
+
responses = kwargs.get("responses")
|
|
41
|
+
if responses is not None:
|
|
42
|
+
if isinstance(responses, str):
|
|
43
|
+
self.responses = [responses]
|
|
44
|
+
else:
|
|
45
|
+
self.responses = responses
|
|
46
|
+
else:
|
|
47
|
+
self.responses = ['{"result": "Mock response"}']
|
|
48
|
+
|
|
49
|
+
# Process mock tool calls
|
|
50
|
+
self.mock_tool_calls = kwargs.get("mock_tool_calls")
|
|
51
|
+
|
|
52
|
+
# Non-config state
|
|
53
|
+
self.call_count = 0
|
|
54
|
+
self.last_messages: MessageList | None = None
|
|
55
|
+
self.should_raise = False
|
|
56
|
+
|
|
57
|
+
async def aresponse(self, messages: MessageList) -> str | None:
|
|
58
|
+
"""Return a response based on the configured responses.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
messages : MessageList
|
|
63
|
+
List of messages to process
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
str | None
|
|
68
|
+
Mock response string or None
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
Exception
|
|
73
|
+
When should_raise is True for testing error conditions
|
|
74
|
+
"""
|
|
75
|
+
self.last_messages = messages
|
|
76
|
+
|
|
77
|
+
if self.delay_seconds > 0:
|
|
78
|
+
await asyncio.sleep(self.delay_seconds)
|
|
79
|
+
|
|
80
|
+
if self.should_raise:
|
|
81
|
+
raise Exception("Mock LLM error for testing")
|
|
82
|
+
|
|
83
|
+
if self.call_count < len(self.responses):
|
|
84
|
+
response = self.responses[self.call_count]
|
|
85
|
+
else:
|
|
86
|
+
response = self.responses[-1] # Repeat last response
|
|
87
|
+
|
|
88
|
+
self.call_count += 1
|
|
89
|
+
return response
|
|
90
|
+
|
|
91
|
+
async def aresponse_with_tools(
|
|
92
|
+
self,
|
|
93
|
+
messages: MessageList,
|
|
94
|
+
tools: list[dict[str, Any]],
|
|
95
|
+
tool_choice: str | dict[str, Any] = "auto",
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""Mock implementation of tool calling with configurable tool call simulation.
|
|
98
|
+
|
|
99
|
+
For testing purposes, this can simulate tool calls based on configuration.
|
|
100
|
+
If mock_tool_calls are configured, it will return those. Otherwise, it
|
|
101
|
+
returns a regular response without tool calls.
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
--------
|
|
105
|
+
Configure mock to return tool calls::
|
|
106
|
+
|
|
107
|
+
mock_llm = MockLLM(
|
|
108
|
+
responses=["I'll search for that"],
|
|
109
|
+
mock_tool_calls=[
|
|
110
|
+
{
|
|
111
|
+
"id": "call_123",
|
|
112
|
+
"name": "search",
|
|
113
|
+
"arguments": {"query": "test"}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
"""
|
|
118
|
+
from hexdag.core.ports.llm import LLMResponse, ToolCall
|
|
119
|
+
|
|
120
|
+
# Get regular response
|
|
121
|
+
response_text = await self.aresponse(messages)
|
|
122
|
+
|
|
123
|
+
# Check if mock tool calls are configured
|
|
124
|
+
mock_tool_calls = getattr(self, "mock_tool_calls", None)
|
|
125
|
+
|
|
126
|
+
if mock_tool_calls and self.call_count <= len(mock_tool_calls):
|
|
127
|
+
# Return configured tool calls
|
|
128
|
+
tool_call_data = (
|
|
129
|
+
mock_tool_calls[self.call_count - 1]
|
|
130
|
+
if self.call_count <= len(mock_tool_calls)
|
|
131
|
+
else mock_tool_calls[-1]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if isinstance(tool_call_data, dict):
|
|
135
|
+
tool_calls_list = [
|
|
136
|
+
ToolCall(
|
|
137
|
+
id=tool_call_data.get("id", "call_mock"),
|
|
138
|
+
name=tool_call_data.get("name", "mock_tool"),
|
|
139
|
+
arguments=tool_call_data.get("arguments", {}),
|
|
140
|
+
)
|
|
141
|
+
]
|
|
142
|
+
elif isinstance(tool_call_data, list):
|
|
143
|
+
tool_calls_list = [
|
|
144
|
+
ToolCall(
|
|
145
|
+
id=tc.get("id", f"call_mock_{i}"),
|
|
146
|
+
name=tc.get("name", "mock_tool"),
|
|
147
|
+
arguments=tc.get("arguments", {}),
|
|
148
|
+
)
|
|
149
|
+
for i, tc in enumerate(tool_call_data)
|
|
150
|
+
]
|
|
151
|
+
else:
|
|
152
|
+
tool_calls_list = None
|
|
153
|
+
|
|
154
|
+
return LLMResponse(
|
|
155
|
+
content=response_text,
|
|
156
|
+
tool_calls=tool_calls_list,
|
|
157
|
+
finish_reason="tool_calls" if tool_calls_list else "stop",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Return as LLMResponse without tool calls
|
|
161
|
+
return LLMResponse(content=response_text, tool_calls=None, finish_reason="stop")
|
|
162
|
+
|
|
163
|
+
async def ahealth_check(self) -> "HealthStatus":
|
|
164
|
+
"""Health check for Mock LLM (always healthy)."""
|
|
165
|
+
from hexdag.core.ports.healthcheck import HealthStatus
|
|
166
|
+
|
|
167
|
+
return HealthStatus(
|
|
168
|
+
status="healthy",
|
|
169
|
+
adapter_name="MockLLM",
|
|
170
|
+
latency_ms=0.1,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Testing utilities (not part of the LLM port interface)
|
|
174
|
+
def reset(self) -> None:
|
|
175
|
+
"""Reset the mock state for testing."""
|
|
176
|
+
self.call_count = 0
|
|
177
|
+
self.last_messages = None
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Simple mock tool adapter with predefined responses for testing."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from hexdag.core.ports.tool_router import ToolRouter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MockToolAdapter(ToolRouter):
|
|
9
|
+
"""Mock tool adapter that returns predefined responses.
|
|
10
|
+
|
|
11
|
+
This is a simpler alternative to MockToolRouter, designed for
|
|
12
|
+
unit tests and offline runs where you want predictable,
|
|
13
|
+
predefined responses for specific tool calls.
|
|
14
|
+
|
|
15
|
+
Example
|
|
16
|
+
-------
|
|
17
|
+
Example usage::
|
|
18
|
+
|
|
19
|
+
mock_tools = {
|
|
20
|
+
"search_customers": [{"id": 1, "name": "Alice"}],
|
|
21
|
+
"get_product": {"id": 42, "name": "Widget", "price": 9.99}
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Type annotations for attributes
|
|
26
|
+
default_response: Any | None
|
|
27
|
+
raise_on_unknown: bool
|
|
28
|
+
mock_responses: dict[str, Any]
|
|
29
|
+
call_history: list[dict[str, Any]]
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
mock_responses: dict[str, Any] | None = None,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Initialize the mock tool adapter.
|
|
37
|
+
|
|
38
|
+
Args
|
|
39
|
+
----
|
|
40
|
+
mock_responses: Dictionary mapping tool names to their predefined responses.
|
|
41
|
+
**kwargs: Configuration options (default_response, raise_on_unknown)
|
|
42
|
+
"""
|
|
43
|
+
self.default_response = kwargs.get("default_response")
|
|
44
|
+
self.raise_on_unknown = kwargs.get("raise_on_unknown", False)
|
|
45
|
+
self.mock_responses = mock_responses or {}
|
|
46
|
+
|
|
47
|
+
self.call_history: list[dict[str, Any]] = []
|
|
48
|
+
|
|
49
|
+
async def acall_tool(self, tool_name: str, params: dict[str, Any]) -> Any:
|
|
50
|
+
"""Call a tool and return its predefined response.
|
|
51
|
+
|
|
52
|
+
Args
|
|
53
|
+
----
|
|
54
|
+
tool_name: Name of the tool to call
|
|
55
|
+
params: Parameters passed to the tool (logged but not used)
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
The predefined response for the tool
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
ValueError: If tool not found and raise_on_unknown is True
|
|
64
|
+
"""
|
|
65
|
+
self.call_history.append({
|
|
66
|
+
"tool": tool_name,
|
|
67
|
+
"params": params,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if tool_name in self.mock_responses:
|
|
71
|
+
response = self.mock_responses[tool_name]
|
|
72
|
+
# If response is callable, call it with params
|
|
73
|
+
if callable(response):
|
|
74
|
+
return response(params)
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
if self.raise_on_unknown:
|
|
78
|
+
available = ", ".join(self.mock_responses.keys())
|
|
79
|
+
raise ValueError(f"Unknown tool: '{tool_name}'. Available tools: {available or 'none'}")
|
|
80
|
+
|
|
81
|
+
if self.default_response is not None:
|
|
82
|
+
return self.default_response
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"status": "success",
|
|
86
|
+
"tool": tool_name,
|
|
87
|
+
"message": f"Mock response for {tool_name}",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def get_available_tools(self) -> list[str]:
|
|
91
|
+
"""Get list of available tool names.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
List of tool names that have predefined responses
|
|
96
|
+
"""
|
|
97
|
+
return list(self.mock_responses.keys())
|
|
98
|
+
|
|
99
|
+
def get_tool_schema(self, tool_name: str) -> dict[str, Any]:
|
|
100
|
+
"""Get schema for a specific tool.
|
|
101
|
+
|
|
102
|
+
Since this is a mock adapter, it returns a basic schema.
|
|
103
|
+
|
|
104
|
+
Args
|
|
105
|
+
----
|
|
106
|
+
tool_name: Name of the tool
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
Basic schema for the tool
|
|
111
|
+
"""
|
|
112
|
+
if tool_name in self.mock_responses:
|
|
113
|
+
return {
|
|
114
|
+
"name": tool_name,
|
|
115
|
+
"description": f"Mock tool: {tool_name}",
|
|
116
|
+
"parameters": {}, # Mock doesn't validate parameters
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
def get_all_tool_schemas(self) -> dict[str, dict[str, Any]]:
|
|
122
|
+
"""Get schemas for all available tools.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
Dictionary mapping tool names to their basic schemas
|
|
127
|
+
"""
|
|
128
|
+
return {name: self.get_tool_schema(name) for name in self.mock_responses}
|
|
129
|
+
|
|
130
|
+
# Utility methods for testing
|
|
131
|
+
def set_response(self, tool_name: str, response: Any) -> None:
|
|
132
|
+
"""Set or update the response for a tool.
|
|
133
|
+
|
|
134
|
+
Args
|
|
135
|
+
----
|
|
136
|
+
tool_name: Name of the tool
|
|
137
|
+
response: Response to return (can be callable)
|
|
138
|
+
"""
|
|
139
|
+
self.mock_responses[tool_name] = response
|
|
140
|
+
|
|
141
|
+
def remove_tool(self, tool_name: str) -> None:
|
|
142
|
+
"""Remove a tool from available tools.
|
|
143
|
+
|
|
144
|
+
Args
|
|
145
|
+
----
|
|
146
|
+
tool_name: Name of the tool to remove
|
|
147
|
+
"""
|
|
148
|
+
self.mock_responses.pop(tool_name, None)
|
|
149
|
+
|
|
150
|
+
def clear_history(self) -> None:
|
|
151
|
+
"""Clear the call history."""
|
|
152
|
+
self.call_history.clear()
|
|
153
|
+
|
|
154
|
+
def get_call_count(self, tool_name: str | None = None) -> int:
|
|
155
|
+
"""Get number of times a tool was called.
|
|
156
|
+
|
|
157
|
+
Args
|
|
158
|
+
----
|
|
159
|
+
tool_name: Name of specific tool, or None for all tools
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
Number of calls
|
|
164
|
+
"""
|
|
165
|
+
if tool_name is None:
|
|
166
|
+
return len(self.call_history)
|
|
167
|
+
|
|
168
|
+
return sum(1 for call in self.call_history if call["tool"] == tool_name)
|
|
169
|
+
|
|
170
|
+
def get_last_call(self, tool_name: str | None = None) -> dict[str, Any] | None:
|
|
171
|
+
"""Get the most recent call.
|
|
172
|
+
|
|
173
|
+
Args
|
|
174
|
+
----
|
|
175
|
+
tool_name: Name of specific tool, or None for any tool
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
Last call details or None if no calls
|
|
180
|
+
"""
|
|
181
|
+
if not self.call_history:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
if tool_name is None:
|
|
185
|
+
return self.call_history[-1]
|
|
186
|
+
|
|
187
|
+
# Find last call for specific tool
|
|
188
|
+
for call in reversed(self.call_history):
|
|
189
|
+
if call["tool"] == tool_name:
|
|
190
|
+
return call
|
|
191
|
+
|
|
192
|
+
return None
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Mock tool router implementation for testing."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import asyncio
|
|
5
|
+
import operator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from hexdag.core.exceptions import ResourceNotFoundError
|
|
9
|
+
from hexdag.core.ports.tool_router import ToolRouter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MockToolRouter(ToolRouter):
|
|
13
|
+
"""Mock implementation of ToolRouter for testing."""
|
|
14
|
+
|
|
15
|
+
# Type annotations for attributes
|
|
16
|
+
available_tools: list[str]
|
|
17
|
+
delay_seconds: float
|
|
18
|
+
raise_on_unknown_tool: bool
|
|
19
|
+
tools: dict[str, dict[str, Any]]
|
|
20
|
+
|
|
21
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
22
|
+
"""Initialize mock tool router.
|
|
23
|
+
|
|
24
|
+
Args
|
|
25
|
+
----
|
|
26
|
+
**kwargs: Configuration options
|
|
27
|
+
"""
|
|
28
|
+
self.available_tools = kwargs.get("available_tools", [])
|
|
29
|
+
self.delay_seconds = kwargs.get("delay_seconds", 0.0)
|
|
30
|
+
self.raise_on_unknown_tool = kwargs.get("raise_on_unknown_tool", False)
|
|
31
|
+
|
|
32
|
+
# Default mock tools
|
|
33
|
+
self.tools: dict[str, dict[str, Any]] = {
|
|
34
|
+
"search": {
|
|
35
|
+
"name": "search",
|
|
36
|
+
"description": "Search for information",
|
|
37
|
+
"parameters": {
|
|
38
|
+
"query": {"type": "string", "description": "Search query"},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"calculate": {
|
|
42
|
+
"name": "calculate",
|
|
43
|
+
"description": "Perform calculations",
|
|
44
|
+
"parameters": {
|
|
45
|
+
"expression": {"type": "string", "description": "Math expression"},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"get_weather": {
|
|
49
|
+
"name": "get_weather",
|
|
50
|
+
"description": "Get weather information",
|
|
51
|
+
"parameters": {
|
|
52
|
+
"location": {"type": "string", "description": "Location name"},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for tool_name in self.available_tools:
|
|
58
|
+
if tool_name not in self.tools:
|
|
59
|
+
self.tools[tool_name] = {
|
|
60
|
+
"name": tool_name,
|
|
61
|
+
"description": f"Mock tool: {tool_name}",
|
|
62
|
+
"parameters": {},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.call_history: list[dict[str, Any]] = []
|
|
66
|
+
|
|
67
|
+
async def acall_tool(self, tool_name: str, params: dict[str, Any]) -> Any:
|
|
68
|
+
"""Call a mock tool with parameters.
|
|
69
|
+
|
|
70
|
+
Args
|
|
71
|
+
----
|
|
72
|
+
tool_name: The name of the tool to call
|
|
73
|
+
params: Parameters to pass to the tool
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
Mock result based on the tool
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
ResourceNotFoundError
|
|
82
|
+
If tool not found and raise_on_unknown_tool is True
|
|
83
|
+
"""
|
|
84
|
+
if self.delay_seconds > 0:
|
|
85
|
+
await asyncio.sleep(self.delay_seconds)
|
|
86
|
+
|
|
87
|
+
self.call_history.append({
|
|
88
|
+
"tool": tool_name,
|
|
89
|
+
"params": params,
|
|
90
|
+
"timestamp": asyncio.get_event_loop().time(),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if tool_name not in self.tools:
|
|
94
|
+
if self.raise_on_unknown_tool:
|
|
95
|
+
raise ResourceNotFoundError("tool", tool_name, list(self.tools.keys()))
|
|
96
|
+
return {"error": f"Unknown tool: {tool_name}"}
|
|
97
|
+
|
|
98
|
+
if tool_name == "search":
|
|
99
|
+
query = params.get("query", "")
|
|
100
|
+
return {
|
|
101
|
+
"results": [
|
|
102
|
+
{"title": f"Result 1 for {query}", "url": "http://example.com/1"},
|
|
103
|
+
{"title": f"Result 2 for {query}", "url": "http://example.com/2"},
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
if tool_name == "calculate":
|
|
107
|
+
expression = params.get("expression", "0")
|
|
108
|
+
try:
|
|
109
|
+
# Safe evaluation using ast for simple math expressions
|
|
110
|
+
# Supports: +, -, *, /, //, %, **, and numbers
|
|
111
|
+
|
|
112
|
+
ops: dict[type[ast.operator] | type[ast.unaryop], Any] = {
|
|
113
|
+
ast.Add: operator.add,
|
|
114
|
+
ast.Sub: operator.sub,
|
|
115
|
+
ast.Mult: operator.mul,
|
|
116
|
+
ast.Div: operator.truediv,
|
|
117
|
+
ast.FloorDiv: operator.floordiv,
|
|
118
|
+
ast.Mod: operator.mod,
|
|
119
|
+
ast.Pow: operator.pow,
|
|
120
|
+
ast.USub: operator.neg,
|
|
121
|
+
ast.UAdd: operator.pos,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def safe_eval(node: ast.AST) -> float | int:
|
|
125
|
+
if isinstance(node, ast.Constant): # Python 3.8+
|
|
126
|
+
val = node.value
|
|
127
|
+
if not isinstance(val, (int, float)):
|
|
128
|
+
raise ValueError(f"Only numeric constants are allowed, got {type(val)}")
|
|
129
|
+
return val
|
|
130
|
+
if isinstance(node, ast.BinOp):
|
|
131
|
+
left = safe_eval(node.left)
|
|
132
|
+
right = safe_eval(node.right)
|
|
133
|
+
op_func = ops.get(type(node.op))
|
|
134
|
+
if op_func is None:
|
|
135
|
+
raise ValueError(f"Unsupported binary operation: {ast.dump(node)}")
|
|
136
|
+
result = op_func(left, right)
|
|
137
|
+
if not isinstance(result, (int, float)):
|
|
138
|
+
raise ValueError("Operation resulted in non-numeric value")
|
|
139
|
+
return result
|
|
140
|
+
if isinstance(node, ast.UnaryOp):
|
|
141
|
+
operand = safe_eval(node.operand)
|
|
142
|
+
op_func = ops.get(type(node.op))
|
|
143
|
+
if op_func is None:
|
|
144
|
+
raise ValueError(f"Unsupported unary operation: {ast.dump(node)}")
|
|
145
|
+
result = op_func(operand)
|
|
146
|
+
if not isinstance(result, (int, float)):
|
|
147
|
+
raise ValueError("Operation resulted in non-numeric value")
|
|
148
|
+
return result
|
|
149
|
+
raise ValueError(f"Unsupported operation: {ast.dump(node)}")
|
|
150
|
+
|
|
151
|
+
tree = ast.parse(expression, mode="eval")
|
|
152
|
+
result = safe_eval(tree.body)
|
|
153
|
+
return {"result": result}
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return {"error": str(e)}
|
|
156
|
+
elif tool_name == "get_weather":
|
|
157
|
+
location = params.get("location", "Unknown")
|
|
158
|
+
return {
|
|
159
|
+
"location": location,
|
|
160
|
+
"temperature": 22,
|
|
161
|
+
"conditions": "Partly cloudy",
|
|
162
|
+
"humidity": 65,
|
|
163
|
+
}
|
|
164
|
+
else:
|
|
165
|
+
# Generic mock response for custom tools
|
|
166
|
+
return {
|
|
167
|
+
"tool": tool_name,
|
|
168
|
+
"status": "success",
|
|
169
|
+
"result": f"Mock result for {tool_name}",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def get_available_tools(self) -> list[str]:
|
|
173
|
+
"""Get list of available mock tool names.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
List of tool names
|
|
178
|
+
"""
|
|
179
|
+
return list(self.tools.keys())
|
|
180
|
+
|
|
181
|
+
def get_tool_schema(self, tool_name: str) -> dict[str, Any]:
|
|
182
|
+
"""Get schema for a specific mock tool.
|
|
183
|
+
|
|
184
|
+
Args
|
|
185
|
+
----
|
|
186
|
+
tool_name: Name of the tool
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
Tool schema dictionary
|
|
191
|
+
|
|
192
|
+
Raises
|
|
193
|
+
------
|
|
194
|
+
KeyError
|
|
195
|
+
If tool not found
|
|
196
|
+
"""
|
|
197
|
+
if tool_name not in self.tools:
|
|
198
|
+
raise KeyError(f"Tool not found: {tool_name}")
|
|
199
|
+
return self.tools[tool_name].copy()
|
|
200
|
+
|
|
201
|
+
def get_all_tool_schemas(self) -> dict[str, dict[str, Any]]:
|
|
202
|
+
"""Get schemas for all available mock tools.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
Dictionary mapping tool names to their schemas
|
|
207
|
+
"""
|
|
208
|
+
return {name: schema.copy() for name, schema in self.tools.items()}
|
|
209
|
+
|
|
210
|
+
# Testing utilities
|
|
211
|
+
def reset(self) -> None:
|
|
212
|
+
"""Reset call history for testing."""
|
|
213
|
+
self.call_history.clear()
|
|
214
|
+
|
|
215
|
+
def get_call_history(self) -> list[dict[str, Any]]:
|
|
216
|
+
"""Get the history of tool calls for testing."""
|
|
217
|
+
return self.call_history.copy()
|
|
218
|
+
|
|
219
|
+
def add_tool(self, name: str, description: str, parameters: dict[str, Any]) -> None:
|
|
220
|
+
"""Add a new mock tool for testing.
|
|
221
|
+
|
|
222
|
+
Args
|
|
223
|
+
----
|
|
224
|
+
name: Tool name
|
|
225
|
+
description: Tool description
|
|
226
|
+
parameters: Tool parameter schema
|
|
227
|
+
"""
|
|
228
|
+
self.tools[name] = {
|
|
229
|
+
"name": name,
|
|
230
|
+
"description": description,
|
|
231
|
+
"parameters": parameters,
|
|
232
|
+
}
|