tactus 0.31.0__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 (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.0.dist-info/METADATA +1809 -0
  157. tactus-0.31.0.dist-info/RECORD +160 -0
  158. tactus-0.31.0.dist-info/WHEEL +4 -0
  159. tactus-0.31.0.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,234 @@
1
+ """
2
+ Mock implementations of dependencies for testing.
3
+
4
+ Provides fake HTTP clients, databases, etc. that can be used in BDD tests
5
+ without making real network calls or database connections.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Any, Optional, List
10
+ from dataclasses import dataclass
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class MockResponse:
17
+ """Mock HTTP response."""
18
+
19
+ text: str
20
+ status_code: int = 200
21
+ headers: Dict[str, str] = None
22
+
23
+ def __post_init__(self):
24
+ if self.headers is None:
25
+ self.headers = {}
26
+
27
+ def json(self):
28
+ """Parse response as JSON."""
29
+ import json
30
+
31
+ return json.loads(self.text)
32
+
33
+
34
+ class MockHTTPClient:
35
+ """
36
+ Mock HTTP client that returns pre-configured responses.
37
+
38
+ Used in tests to avoid making real HTTP calls.
39
+ """
40
+
41
+ def __init__(self, responses: Optional[Dict[str, str]] = None):
42
+ """
43
+ Initialize mock HTTP client.
44
+
45
+ Args:
46
+ responses: Dict mapping path to response text
47
+ e.g., {"/weather": '{"temp": 72}'}
48
+ """
49
+ self.responses = responses or {}
50
+ self.calls: List[tuple] = [] # Track all calls for assertions
51
+ self.base_url = None
52
+
53
+ def add_response(self, path: str, response: str, status_code: int = 200):
54
+ """Add a mock response for a specific path."""
55
+ self.responses[path] = {"text": response, "status_code": status_code}
56
+ logger.debug(f"Added mock response for {path}")
57
+
58
+ async def get(self, path: str, **kwargs) -> MockResponse:
59
+ """Mock GET request."""
60
+ self.calls.append(("GET", path, kwargs))
61
+ logger.debug(f"Mock HTTP GET: {path}")
62
+
63
+ if path in self.responses:
64
+ response_data = self.responses[path]
65
+ if isinstance(response_data, dict):
66
+ return MockResponse(
67
+ text=response_data.get("text", ""),
68
+ status_code=response_data.get("status_code", 200),
69
+ )
70
+ else:
71
+ return MockResponse(text=response_data)
72
+
73
+ # Default response if no mock configured
74
+ return MockResponse(text="{}", status_code=200)
75
+
76
+ async def post(self, path: str, **kwargs) -> MockResponse:
77
+ """Mock POST request."""
78
+ self.calls.append(("POST", path, kwargs))
79
+ logger.debug(f"Mock HTTP POST: {path}")
80
+
81
+ if path in self.responses:
82
+ response_data = self.responses[path]
83
+ if isinstance(response_data, dict):
84
+ return MockResponse(
85
+ text=response_data.get("text", ""),
86
+ status_code=response_data.get("status_code", 200),
87
+ )
88
+ else:
89
+ return MockResponse(text=response_data)
90
+
91
+ return MockResponse(text="{}", status_code=200)
92
+
93
+ async def aclose(self):
94
+ """Mock close method (does nothing)."""
95
+ logger.debug("Mock HTTP client closed")
96
+ pass
97
+
98
+ def get_calls(self) -> List[tuple]:
99
+ """Get all calls made to this client."""
100
+ return self.calls
101
+
102
+ def was_called(self, method: str = None, path: str = None) -> bool:
103
+ """Check if a specific call was made."""
104
+ for call_method, call_path, _ in self.calls:
105
+ if method and method != call_method:
106
+ continue
107
+ if path and path != call_path:
108
+ continue
109
+ return True
110
+ return False
111
+
112
+
113
+ class MockDatabase:
114
+ """
115
+ Mock database connection for testing.
116
+
117
+ Stores data in memory instead of making real database calls.
118
+ """
119
+
120
+ def __init__(self):
121
+ self.data: Dict[str, Any] = {}
122
+ self.queries: List[str] = []
123
+
124
+ async def execute(self, query: str, *args) -> Any:
125
+ """Mock query execution."""
126
+ self.queries.append(query)
127
+ logger.debug(f"Mock DB execute: {query}")
128
+ return None
129
+
130
+ async def fetch(self, query: str, *args) -> List[Dict]:
131
+ """Mock fetch (returns empty list)."""
132
+ self.queries.append(query)
133
+ logger.debug(f"Mock DB fetch: {query}")
134
+ return []
135
+
136
+ async def close(self):
137
+ """Mock close."""
138
+ logger.debug("Mock database closed")
139
+ pass
140
+
141
+
142
+ class MockRedis:
143
+ """
144
+ Mock Redis client for testing.
145
+
146
+ Stores data in memory dictionary.
147
+ """
148
+
149
+ def __init__(self):
150
+ self.store: Dict[str, Any] = {}
151
+
152
+ async def get(self, key: str) -> Optional[str]:
153
+ """Mock get."""
154
+ return self.store.get(key)
155
+
156
+ async def set(self, key: str, value: Any):
157
+ """Mock set."""
158
+ self.store[key] = value
159
+ logger.debug(f"Mock Redis SET: {key}")
160
+
161
+ async def delete(self, key: str):
162
+ """Mock delete."""
163
+ if key in self.store:
164
+ del self.store[key]
165
+ logger.debug(f"Mock Redis DEL: {key}")
166
+
167
+ async def close(self):
168
+ """Mock close."""
169
+ logger.debug("Mock Redis closed")
170
+ pass
171
+
172
+
173
+ class MockDependencyFactory:
174
+ """
175
+ Factory for creating mock dependencies instead of real ones.
176
+
177
+ Used by test infrastructure to inject mocks.
178
+ """
179
+
180
+ @staticmethod
181
+ async def create_mock(
182
+ resource_type: str, config: Dict[str, Any], mock_responses: Optional[Dict] = None
183
+ ) -> Any:
184
+ """
185
+ Create a mock dependency.
186
+
187
+ Args:
188
+ resource_type: Type of resource (http_client, postgres, redis)
189
+ config: Configuration dict (mostly ignored for mocks)
190
+ mock_responses: Optional dict of mock responses for HTTP client
191
+
192
+ Returns:
193
+ Mock resource instance
194
+ """
195
+ if resource_type == "http_client":
196
+ mock_client = MockHTTPClient(mock_responses)
197
+ mock_client.base_url = config.get("base_url")
198
+ return mock_client
199
+
200
+ elif resource_type == "postgres":
201
+ return MockDatabase()
202
+
203
+ elif resource_type == "redis":
204
+ return MockRedis()
205
+
206
+ else:
207
+ raise ValueError(f"Unknown resource type: {resource_type}")
208
+
209
+ @staticmethod
210
+ async def create_all_mocks(
211
+ dependencies_config: Dict[str, Dict[str, Any]],
212
+ mock_responses: Optional[Dict[str, Dict]] = None,
213
+ ) -> Dict[str, Any]:
214
+ """
215
+ Create all mock dependencies.
216
+
217
+ Args:
218
+ dependencies_config: Dict mapping dependency name to config
219
+ mock_responses: Optional dict mapping dependency name to mock responses
220
+
221
+ Returns:
222
+ Dict mapping dependency name to mock resource
223
+ """
224
+ mocks = {}
225
+
226
+ for dep_name, dep_config in dependencies_config.items():
227
+ resource_type = dep_config.get("type")
228
+ responses = mock_responses.get(dep_name) if mock_responses else None
229
+
230
+ mock = await MockDependencyFactory.create_mock(resource_type, dep_config, responses)
231
+ mocks[dep_name] = mock
232
+ logger.info(f"Created mock dependency '{dep_name}' of type '{resource_type}'")
233
+
234
+ return mocks
@@ -0,0 +1,171 @@
1
+ """
2
+ Mock HITL handler for BDD testing.
3
+
4
+ Provides automatic responses for human interactions during tests,
5
+ allowing tests to run without human intervention.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime
10
+ from typing import Any, Dict, Optional
11
+
12
+ from tactus.protocols.models import HITLRequest, HITLResponse
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MockHITLHandler:
19
+ """
20
+ HITL handler that provides automatic responses for tests.
21
+
22
+ Useful for:
23
+ - Running tests without human intervention
24
+ - Deterministic test behavior
25
+ - Fast test execution
26
+ """
27
+
28
+ def __init__(self, default_responses: Optional[Dict[str, Any]] = None):
29
+ """
30
+ Initialize mock HITL handler.
31
+
32
+ Args:
33
+ default_responses: Dict of request_id -> response value
34
+ If not provided, uses sensible defaults
35
+ """
36
+ self.default_responses = default_responses or {}
37
+ self.requests_received: list[HITLRequest] = []
38
+
39
+ def request_interaction(self, procedure_id: str, request: HITLRequest) -> HITLResponse:
40
+ """
41
+ Handle HITL request with automatic response.
42
+
43
+ Args:
44
+ procedure_id: Unique procedure identifier
45
+ request: HITLRequest with interaction details
46
+
47
+ Returns:
48
+ HITLResponse with automatic answer
49
+ """
50
+ # Record the request
51
+ self.requests_received.append(request)
52
+
53
+ logger.debug(
54
+ f"Mock HITL request: type={request.request_type}, message={request.message[:50]}..."
55
+ )
56
+
57
+ # Determine response based on request type
58
+ if request.request_type == "approval":
59
+ value = self._get_response(request, default=True)
60
+ elif request.request_type == "input":
61
+ value = self._get_response(request, default="test input")
62
+ elif request.request_type == "review":
63
+ value = self._get_response(request, default={"decision": "Approve"})
64
+ elif request.request_type == "notification":
65
+ value = self._get_response(request, default=None)
66
+ elif request.request_type == "escalation":
67
+ value = self._get_response(request, default={"escalated": True})
68
+ else:
69
+ value = self._get_response(request, default=None)
70
+
71
+ logger.info(f"Mock HITL response: {value}")
72
+
73
+ return HITLResponse(
74
+ value=value,
75
+ responded_at=datetime.utcnow(),
76
+ timed_out=False,
77
+ )
78
+
79
+ def _get_response(self, request: HITLRequest, default: Any) -> Any:
80
+ """
81
+ Get response for request, checking custom responses first.
82
+
83
+ Args:
84
+ request: The HITL request
85
+ default: Default value if no custom response
86
+
87
+ Returns:
88
+ Response value
89
+ """
90
+ # Check if we have a custom response for this message
91
+ # Use message as key for lookup
92
+ message_key = request.message[:50] # Use first 50 chars as key
93
+
94
+ if message_key in self.default_responses:
95
+ return self.default_responses[message_key]
96
+
97
+ # Check for type-based default
98
+ type_key = f"_type_{request.request_type}"
99
+ if type_key in self.default_responses:
100
+ return self.default_responses[type_key]
101
+
102
+ # Use default
103
+ return default
104
+
105
+ def check_pending_response(self, procedure_id: str, message_id: str) -> Optional[HITLResponse]:
106
+ """
107
+ Check for pending response (not used in tests).
108
+
109
+ Args:
110
+ procedure_id: Unique procedure identifier
111
+ message_id: Message/request ID to check
112
+
113
+ Returns:
114
+ None (tests don't have pending responses)
115
+ """
116
+ return None
117
+
118
+ def cancel_pending_request(self, procedure_id: str, message_id: str) -> None:
119
+ """
120
+ Cancel pending request (not used in tests).
121
+
122
+ Args:
123
+ procedure_id: Unique procedure identifier
124
+ message_id: Message/request ID to cancel
125
+ """
126
+ pass
127
+
128
+ def get_requests_received(self) -> list[HITLRequest]:
129
+ """
130
+ Get all HITL requests received during test.
131
+
132
+ Returns:
133
+ List of HITLRequest objects
134
+ """
135
+ return self.requests_received
136
+
137
+ def clear_history(self) -> None:
138
+ """Clear request history."""
139
+ self.requests_received.clear()
140
+
141
+ def configure_response(self, interaction_type: str, value: Any) -> None:
142
+ """
143
+ Configure mock response for a specific interaction type.
144
+
145
+ This allows dynamic configuration during test scenarios.
146
+
147
+ Args:
148
+ interaction_type: Type of interaction (approval, input, review, etc.)
149
+ value: The value to return for this interaction type
150
+
151
+ Example:
152
+ mock_hitl.configure_response("approval", True)
153
+ mock_hitl.configure_response("input", "test data")
154
+ """
155
+ type_key = f"_type_{interaction_type}"
156
+ self.default_responses[type_key] = value
157
+ logger.debug(f"Configured mock HITL response: {interaction_type} -> {value}")
158
+
159
+ def configure_message_response(self, message_prefix: str, value: Any) -> None:
160
+ """
161
+ Configure mock response for a specific message.
162
+
163
+ Args:
164
+ message_prefix: Prefix of the message to match
165
+ value: The value to return when this message is received
166
+
167
+ Example:
168
+ mock_hitl.configure_message_response("Approve payment", False)
169
+ """
170
+ self.default_responses[message_prefix] = value
171
+ logger.debug(f"Configured mock HITL response for message: {message_prefix}")
@@ -0,0 +1,168 @@
1
+ """
2
+ Unified mock registry for managing all mocks (dependencies + HITL).
3
+
4
+ This provides a central place to configure mocks for both dependencies
5
+ and HITL interactions, usable in BDD tests and evaluations.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Any, Optional
10
+ from tactus.testing.mock_dependencies import MockHTTPClient, MockDatabase, MockRedis
11
+ from tactus.testing.mock_hitl import MockHITLHandler
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class UnifiedMockRegistry:
17
+ """
18
+ Central registry for all mocks (dependencies + HITL).
19
+
20
+ This allows test scenarios to configure mock responses via
21
+ Gherkin steps or programmatically.
22
+ """
23
+
24
+ def __init__(self, hitl_handler: Optional[MockHITLHandler] = None):
25
+ """
26
+ Initialize unified mock registry.
27
+
28
+ Args:
29
+ hitl_handler: Optional existing HITL handler to use
30
+ """
31
+ # HTTP dependency mocks (dep_name -> MockHTTPClient)
32
+ self.http_mocks: Dict[str, MockHTTPClient] = {}
33
+
34
+ # Database mocks
35
+ self.db_mocks: Dict[str, MockDatabase] = {}
36
+
37
+ # Redis mocks
38
+ self.redis_mocks: Dict[str, MockRedis] = {}
39
+
40
+ # HITL mock handler
41
+ self.hitl_mock: MockHITLHandler = hitl_handler or MockHITLHandler()
42
+
43
+ # Store all created mocks for cleanup
44
+ self.all_mocks: Dict[str, Any] = {}
45
+
46
+ def configure_http_response(
47
+ self, dep_name: str, path: Optional[str], response: str, status_code: int = 200
48
+ ) -> None:
49
+ """
50
+ Configure mock HTTP response via Gherkin step.
51
+
52
+ Args:
53
+ dep_name: Name of the HTTP dependency
54
+ path: URL path (or None for default response)
55
+ response: Response text (usually JSON string)
56
+ status_code: HTTP status code
57
+
58
+ Example:
59
+ registry.configure_http_response("weather_api", "/data", '{"temp": 72}')
60
+ """
61
+ if dep_name not in self.http_mocks:
62
+ self.http_mocks[dep_name] = MockHTTPClient()
63
+
64
+ if path:
65
+ self.http_mocks[dep_name].add_response(path, response, status_code)
66
+ else:
67
+ # Set default response for any path
68
+ self.http_mocks[dep_name].responses["_default"] = {
69
+ "text": response,
70
+ "status_code": status_code,
71
+ }
72
+
73
+ logger.debug(f"Configured mock HTTP response for {dep_name}: {path} -> {response[:50]}...")
74
+
75
+ def configure_hitl_response(self, interaction_type: str, value: Any) -> None:
76
+ """
77
+ Configure HITL mock response via Gherkin step.
78
+
79
+ Args:
80
+ interaction_type: Type of interaction (approval, input, review)
81
+ value: The value to return
82
+
83
+ Example:
84
+ registry.configure_hitl_response("approval", True)
85
+ """
86
+ self.hitl_mock.configure_response(interaction_type, value)
87
+
88
+ def configure_hitl_message_response(self, message_prefix: str, value: Any) -> None:
89
+ """
90
+ Configure HITL mock response for specific message.
91
+
92
+ Args:
93
+ message_prefix: Prefix of the message to match
94
+ value: The value to return
95
+
96
+ Example:
97
+ registry.configure_hitl_message_response("Approve payment", False)
98
+ """
99
+ self.hitl_mock.configure_message_response(message_prefix, value)
100
+
101
+ async def create_mock_dependencies(
102
+ self, dependencies_config: Dict[str, Dict[str, Any]]
103
+ ) -> Dict[str, Any]:
104
+ """
105
+ Create mock dependencies for runtime.
106
+
107
+ This is called by the test runner to create mocks based on
108
+ the procedure's dependency declarations.
109
+
110
+ Args:
111
+ dependencies_config: Dict mapping dependency name to config
112
+
113
+ Returns:
114
+ Dict mapping dependency name to mock resource
115
+ """
116
+ mocks = {}
117
+
118
+ for dep_name, dep_config in dependencies_config.items():
119
+ resource_type = dep_config.get("type")
120
+
121
+ if resource_type == "http_client":
122
+ # Use pre-configured mock if it exists, otherwise create new one
123
+ if dep_name in self.http_mocks:
124
+ mock = self.http_mocks[dep_name]
125
+ else:
126
+ mock = MockHTTPClient()
127
+ self.http_mocks[dep_name] = mock
128
+
129
+ mock.base_url = dep_config.get("base_url")
130
+ mocks[dep_name] = mock
131
+
132
+ elif resource_type == "postgres":
133
+ if dep_name not in self.db_mocks:
134
+ self.db_mocks[dep_name] = MockDatabase()
135
+ mocks[dep_name] = self.db_mocks[dep_name]
136
+
137
+ elif resource_type == "redis":
138
+ if dep_name not in self.redis_mocks:
139
+ self.redis_mocks[dep_name] = MockRedis()
140
+ mocks[dep_name] = self.redis_mocks[dep_name]
141
+
142
+ else:
143
+ logger.warning(
144
+ f"Unknown resource type '{resource_type}' for dependency '{dep_name}'"
145
+ )
146
+ continue
147
+
148
+ self.all_mocks[dep_name] = mocks[dep_name]
149
+ logger.info(f"Created mock dependency '{dep_name}' of type '{resource_type}'")
150
+
151
+ return mocks
152
+
153
+ def get_hitl_handler(self) -> MockHITLHandler:
154
+ """Get the HITL mock handler."""
155
+ return self.hitl_mock
156
+
157
+ def clear_all(self) -> None:
158
+ """Clear all mock configurations and history."""
159
+ self.http_mocks.clear()
160
+ self.db_mocks.clear()
161
+ self.redis_mocks.clear()
162
+ self.hitl_mock.clear_history()
163
+ self.all_mocks.clear()
164
+ logger.debug("Cleared all mocks")
165
+
166
+ def get_mock(self, dep_name: str) -> Optional[Any]:
167
+ """Get a specific mock by name."""
168
+ return self.all_mocks.get(dep_name)
@@ -0,0 +1,133 @@
1
+ """
2
+ Mock tool system for deterministic BDD testing.
3
+
4
+ Provides mocked tool responses for fast, repeatable tests
5
+ without requiring actual LLM calls or external services.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict
10
+
11
+ from tactus.primitives.tool import ToolPrimitive, ToolCall
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MockToolRegistry:
18
+ """
19
+ Registry for mock tool responses.
20
+
21
+ Maps tool names to mock responses (static or callable).
22
+ """
23
+
24
+ def __init__(self):
25
+ self.mocks: Dict[str, Any] = {}
26
+
27
+ def register(self, tool_name: str, response: Any) -> None:
28
+ """
29
+ Register a mock response for a tool.
30
+
31
+ Args:
32
+ tool_name: Name of the tool to mock
33
+ response: Mock response (can be static value or callable)
34
+ """
35
+ self.mocks[tool_name] = response
36
+ logger.debug(f"Registered mock for tool: {tool_name}")
37
+
38
+ def get_response(self, tool_name: str, args: Dict) -> Any:
39
+ """
40
+ Get mock response for tool call.
41
+
42
+ Args:
43
+ tool_name: Name of the tool
44
+ args: Arguments passed to the tool
45
+
46
+ Returns:
47
+ Mock response
48
+
49
+ Raises:
50
+ ValueError: If no mock registered for tool
51
+ """
52
+ if tool_name not in self.mocks:
53
+ raise ValueError(f"No mock registered for tool: {tool_name}")
54
+
55
+ response = self.mocks[tool_name]
56
+
57
+ # Support callable mocks for dynamic responses
58
+ if callable(response):
59
+ return response(args)
60
+
61
+ return response
62
+
63
+ def has_mock(self, tool_name: str) -> bool:
64
+ """Check if tool has a mock registered."""
65
+ return tool_name in self.mocks
66
+
67
+ def clear(self) -> None:
68
+ """Clear all registered mocks."""
69
+ self.mocks.clear()
70
+
71
+
72
+ class MockedToolPrimitive(ToolPrimitive):
73
+ """
74
+ Tool primitive that uses mocked responses instead of real tool execution.
75
+
76
+ Useful for:
77
+ - Fast, deterministic tests
78
+ - Testing without API keys
79
+ - Avoiding external service calls
80
+ """
81
+
82
+ def __init__(self, mock_registry: MockToolRegistry):
83
+ super().__init__()
84
+ self.mock_registry = mock_registry
85
+
86
+ def record_call(
87
+ self, tool_name: str, args: Dict[str, Any], result: Any = None, agent_name: str = None
88
+ ) -> Any:
89
+ """
90
+ Record tool call and return mock response.
91
+
92
+ Args:
93
+ tool_name: Name of the tool
94
+ args: Tool arguments
95
+ result: Optional result (ignored in mock mode - we use mock registry)
96
+ agent_name: Optional agent name (for compatibility with base class)
97
+
98
+ Returns:
99
+ Mocked tool result (or default if no mock registered)
100
+ """
101
+ # Get mock response, or use default if not registered
102
+ # Ignore the passed result - we always use mock responses
103
+ if self.mock_registry.has_mock(tool_name):
104
+ mock_result = self.mock_registry.get_response(tool_name, args)
105
+ else:
106
+ # No mock registered - use a default response
107
+ # This allows agent mocks to call tools that don't have explicit mocks
108
+ mock_result = {"status": "ok", "tool": tool_name}
109
+ logger.debug(f"No mock registered for {tool_name}, using default response")
110
+
111
+ # Record the call (same as real ToolPrimitive)
112
+ call = ToolCall(tool_name, args, mock_result)
113
+ self._tool_calls.append(call)
114
+ self._last_calls[tool_name] = call
115
+
116
+ logger.info(f"Mocked tool call: {tool_name}(args={args}) -> {mock_result}")
117
+
118
+ return mock_result
119
+
120
+
121
+ def create_default_mocks() -> Dict[str, Any]:
122
+ """
123
+ Create default mock responses for common tools.
124
+
125
+ Returns:
126
+ Dict of tool_name -> mock_response
127
+ """
128
+ return {
129
+ "done": {"status": "complete", "message": "Task completed"},
130
+ "search": {"results": ["result1", "result2", "result3"]},
131
+ "write_file": {"success": True, "path": "/tmp/test.txt"},
132
+ "read_file": {"content": "test content"},
133
+ }