hexdag 0.5.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,5 @@
1
+ """OpenAI adapters for hexDAG."""
2
+
3
+ from hexdag.builtin.adapters.openai.openai_adapter import OpenAIAdapter
4
+
5
+ __all__ = ["OpenAIAdapter"]