tactus 0.31.2__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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.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
|
+
}
|