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,236 @@
1
+ """
2
+ Message history management for per-agent conversation histories.
3
+
4
+ Manages conversation histories with filtering capabilities for
5
+ token budgets, message limits, and custom filters.
6
+
7
+ Aligned with pydantic-ai's message_history concept.
8
+ """
9
+
10
+ from typing import Any, Optional
11
+
12
+ try:
13
+ from pydantic_ai.messages import ModelMessage
14
+ except ImportError:
15
+ # Fallback if pydantic_ai not available
16
+ ModelMessage = dict
17
+
18
+ from .registry import MessageHistoryConfiguration
19
+
20
+
21
+ class MessageHistoryManager:
22
+ """Manages per-agent message histories with filtering.
23
+
24
+ Aligned with pydantic-ai's message_history concept - this manager
25
+ maintains the message_history lists that get passed to agent.run_sync().
26
+ """
27
+
28
+ def __init__(self):
29
+ """Initialize message history manager."""
30
+ self.histories: dict[str, list[ModelMessage]] = {}
31
+ self.shared_history: list[ModelMessage] = []
32
+
33
+ def get_history_for_agent(
34
+ self,
35
+ agent_name: str,
36
+ message_history_config: Optional[MessageHistoryConfiguration] = None,
37
+ context: Optional[Any] = None,
38
+ ) -> list[ModelMessage]:
39
+ """
40
+ Get filtered message history for an agent.
41
+
42
+ This returns the message_history list that will be passed to
43
+ pydantic-ai's agent.run_sync(message_history=...).
44
+
45
+ Args:
46
+ agent_name: Name of the agent
47
+ message_history_config: Message history configuration (source, filter)
48
+ context: Runtime context for filter functions
49
+
50
+ Returns:
51
+ List of messages for the agent (message_history for pydantic-ai)
52
+ """
53
+ if message_history_config is None:
54
+ # Default: own history, no filter
55
+ return self.histories.get(agent_name, [])
56
+
57
+ # Determine source
58
+ if message_history_config.source == "own":
59
+ messages = self.histories.get(agent_name, [])
60
+ elif message_history_config.source == "shared":
61
+ messages = self.shared_history
62
+ else:
63
+ # Another agent's history
64
+ messages = self.histories.get(message_history_config.source, [])
65
+
66
+ # Apply filter if specified
67
+ if message_history_config.filter:
68
+ messages = self._apply_filter(messages, message_history_config.filter, context)
69
+
70
+ return messages
71
+
72
+ def add_message(
73
+ self,
74
+ agent_name: str,
75
+ message: ModelMessage,
76
+ also_shared: bool = False,
77
+ ) -> None:
78
+ """
79
+ Add a message to an agent's history.
80
+
81
+ Args:
82
+ agent_name: Name of the agent
83
+ message: Message to add
84
+ also_shared: Also add to shared history
85
+ """
86
+ if agent_name not in self.histories:
87
+ self.histories[agent_name] = []
88
+
89
+ self.histories[agent_name].append(message)
90
+
91
+ if also_shared:
92
+ self.shared_history.append(message)
93
+
94
+ def clear_agent_history(self, agent_name: str) -> None:
95
+ """Clear an agent's history."""
96
+ self.histories[agent_name] = []
97
+
98
+ def clear_shared_history(self) -> None:
99
+ """Clear shared history."""
100
+ self.shared_history = []
101
+
102
+ def _apply_filter(
103
+ self,
104
+ messages: list[ModelMessage],
105
+ filter_spec: Any,
106
+ context: Optional[Any],
107
+ ) -> list[ModelMessage]:
108
+ """
109
+ Apply declarative or function filter.
110
+
111
+ Args:
112
+ messages: Messages to filter
113
+ filter_spec: Filter specification (tuple or callable)
114
+ context: Runtime context
115
+
116
+ Returns:
117
+ Filtered messages
118
+ """
119
+ # If it's a callable (Lua function), call it
120
+ if callable(filter_spec):
121
+ try:
122
+ return filter_spec(messages, context)
123
+ except Exception as e:
124
+ # If filter fails, return unfiltered
125
+ print(f"Warning: Filter function failed: {e}")
126
+ return messages
127
+
128
+ # Otherwise it's a tuple (filter_type, filter_arg)
129
+ if not isinstance(filter_spec, tuple) or len(filter_spec) < 2:
130
+ return messages
131
+
132
+ filter_type = filter_spec[0]
133
+ filter_arg = filter_spec[1]
134
+
135
+ if filter_type == "last_n":
136
+ return self._filter_last_n(messages, filter_arg)
137
+ elif filter_type == "token_budget":
138
+ return self._filter_by_token_budget(messages, filter_arg)
139
+ elif filter_type == "by_role":
140
+ return self._filter_by_role(messages, filter_arg)
141
+ elif filter_type == "compose":
142
+ # Apply multiple filters in sequence
143
+ result = messages
144
+ for f in filter_arg:
145
+ result = self._apply_filter(result, f, context)
146
+ return result
147
+ else:
148
+ # Unknown filter type, return unfiltered
149
+ return messages
150
+
151
+ def _filter_last_n(
152
+ self,
153
+ messages: list[ModelMessage],
154
+ n: int,
155
+ ) -> list[ModelMessage]:
156
+ """Keep only the last N messages."""
157
+ return messages[-n:] if n > 0 else []
158
+
159
+ def _filter_by_token_budget(
160
+ self,
161
+ messages: list[ModelMessage],
162
+ max_tokens: int,
163
+ ) -> list[ModelMessage]:
164
+ """
165
+ Filter messages to stay within token budget.
166
+
167
+ Uses a simple heuristic: ~4 characters per token.
168
+ Keeps most recent messages that fit within budget.
169
+ """
170
+ if max_tokens <= 0:
171
+ return []
172
+
173
+ # Rough estimate: 4 chars per token
174
+ max_chars = max_tokens * 4
175
+
176
+ result = []
177
+ current_chars = 0
178
+
179
+ # Work backwards from most recent
180
+ for message in reversed(messages):
181
+ # Estimate message size
182
+ message_chars = self._estimate_message_chars(message)
183
+
184
+ if current_chars + message_chars > max_chars:
185
+ # Would exceed budget, stop here
186
+ break
187
+
188
+ result.insert(0, message)
189
+ current_chars += message_chars
190
+
191
+ return result
192
+
193
+ def _filter_by_role(
194
+ self,
195
+ messages: list[ModelMessage],
196
+ role: str,
197
+ ) -> list[ModelMessage]:
198
+ """Keep only messages with specified role."""
199
+ return [m for m in messages if self._get_message_role(m) == role]
200
+
201
+ def _estimate_message_chars(self, message: ModelMessage) -> int:
202
+ """Estimate character count of a message."""
203
+ if isinstance(message, dict):
204
+ # Dict-based message
205
+ content = message.get("content", "")
206
+ if isinstance(content, str):
207
+ return len(content)
208
+ elif isinstance(content, list):
209
+ # Multiple content parts
210
+ total = 0
211
+ for part in content:
212
+ if isinstance(part, dict):
213
+ total += len(str(part.get("text", "")))
214
+ else:
215
+ total += len(str(part))
216
+ return total
217
+ return len(str(content))
218
+ else:
219
+ # Pydantic AI ModelMessage object
220
+ try:
221
+ # Try to access content attribute
222
+ content = getattr(message, "content", "")
223
+ return len(str(content))
224
+ except Exception:
225
+ # Fallback: convert to string
226
+ return len(str(message))
227
+
228
+ def _get_message_role(self, message: ModelMessage) -> str:
229
+ """Get role from a message."""
230
+ if isinstance(message, dict):
231
+ return message.get("role", "")
232
+ else:
233
+ try:
234
+ return getattr(message, "role", "")
235
+ except Exception:
236
+ return ""
tactus/core/mocking.py ADDED
@@ -0,0 +1,286 @@
1
+ """
2
+ Mocking infrastructure for Tactus.
3
+
4
+ Provides comprehensive mocking capabilities including:
5
+ - Static mocks (always return same value)
6
+ - Temporal mocks (different returns per call)
7
+ - Conditional mocks (based on input parameters)
8
+ - Mock state tracking and assertions
9
+ """
10
+
11
+ import logging
12
+ from typing import Any, Dict, List, Optional, Union
13
+ from dataclasses import dataclass, field
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class MockCall:
20
+ """Record of a mock tool call."""
21
+
22
+ tool_name: str
23
+ args: Dict[str, Any]
24
+ result: Any
25
+ call_number: int
26
+ timestamp: float
27
+
28
+
29
+ @dataclass
30
+ class MockConfig:
31
+ """Configuration for a mocked tool."""
32
+
33
+ tool_name: str
34
+
35
+ # Static mock - always returns this
36
+ static_result: Optional[Any] = None
37
+
38
+ # Temporal mocks - return different values per call
39
+ temporal_results: List[Any] = field(default_factory=list)
40
+
41
+ # Conditional mocks - return based on args
42
+ conditional_mocks: List[Dict[str, Any]] = field(default_factory=list)
43
+
44
+ # Error simulation
45
+ error: Optional[str] = None
46
+
47
+ # Whether this mock is enabled
48
+ enabled: bool = True
49
+
50
+
51
+ class MockManager:
52
+ """
53
+ Manages mock state and temporal mocking for testing.
54
+
55
+ This is injected into the runtime to intercept tool calls
56
+ and return mock responses when configured.
57
+ """
58
+
59
+ def __init__(self):
60
+ """Initialize mock manager."""
61
+ self.mocks: Dict[str, MockConfig] = {}
62
+ self.call_history: Dict[str, List[MockCall]] = {}
63
+ self.call_counts: Dict[str, int] = {}
64
+ self.enabled = True # Global mock enable/disable
65
+
66
+ def register_mock(self, tool_name: str, config: Union[MockConfig, Dict[str, Any]]) -> None:
67
+ """
68
+ Register a mock configuration for a tool.
69
+
70
+ Args:
71
+ tool_name: Name of the tool to mock
72
+ config: MockConfig or dict with mock settings
73
+ """
74
+ if isinstance(config, dict):
75
+ # Convert dict to MockConfig
76
+ if "output" in config:
77
+ # Simple static mock
78
+ mock_config = MockConfig(tool_name=tool_name, static_result=config["output"])
79
+ elif "error" in config:
80
+ # Error simulation
81
+ mock_config = MockConfig(tool_name=tool_name, error=config["error"])
82
+ elif "temporal" in config:
83
+ # Temporal mocking
84
+ mock_config = MockConfig(tool_name=tool_name, temporal_results=config["temporal"])
85
+ else:
86
+ # Full config
87
+ mock_config = MockConfig(tool_name=tool_name, **config)
88
+ else:
89
+ mock_config = config
90
+
91
+ self.mocks[tool_name] = mock_config
92
+ logger.info(f"Registered mock for tool '{tool_name}'")
93
+
94
+ def get_mock_response(self, tool_name: str, args: Dict[str, Any]) -> Optional[Any]:
95
+ """
96
+ Get mock response for a tool call.
97
+
98
+ Args:
99
+ tool_name: Name of the tool being called
100
+ args: Arguments passed to the tool
101
+
102
+ Returns:
103
+ Mock response if configured, None if tool should run normally
104
+ """
105
+ if not self.enabled:
106
+ return None
107
+
108
+ if tool_name not in self.mocks:
109
+ return None
110
+
111
+ mock_config = self.mocks[tool_name]
112
+ if not mock_config.enabled:
113
+ return None
114
+
115
+ # Get call number for this tool
116
+ call_number = self.call_counts.get(tool_name, 0) + 1
117
+
118
+ # Check for error simulation
119
+ if mock_config.error:
120
+ raise RuntimeError(mock_config.error)
121
+
122
+ # Check temporal mocks first
123
+ if mock_config.temporal_results:
124
+ # Return based on call number (1-indexed)
125
+ if call_number <= len(mock_config.temporal_results):
126
+ result = mock_config.temporal_results[call_number - 1]
127
+ logger.debug(
128
+ f"Mock '{tool_name}' returning temporal result for call {call_number}: {result}"
129
+ )
130
+ return result
131
+ else:
132
+ # Fallback to last result if we've exceeded temporal results
133
+ result = mock_config.temporal_results[-1]
134
+ logger.debug(
135
+ f"Mock '{tool_name}' returning last temporal result (call {call_number}): {result}"
136
+ )
137
+ return result
138
+
139
+ # Check conditional mocks
140
+ for conditional in mock_config.conditional_mocks:
141
+ condition = conditional.get("when", {})
142
+ if self._matches_condition(args, condition):
143
+ result = conditional.get("return")
144
+ logger.debug(f"Mock '{tool_name}' matched condition, returning: {result}")
145
+ return result
146
+
147
+ # Return static result if configured
148
+ if mock_config.static_result is not None:
149
+ logger.debug(f"Mock '{tool_name}' returning static result: {mock_config.static_result}")
150
+ return mock_config.static_result
151
+
152
+ # No mock response configured
153
+ return None
154
+
155
+ def record_call(self, tool_name: str, args: Dict[str, Any], result: Any) -> None:
156
+ """
157
+ Record a tool call for assertions.
158
+
159
+ Args:
160
+ tool_name: Name of the tool that was called
161
+ args: Arguments passed to the tool
162
+ result: Result returned by the tool (or mock)
163
+ """
164
+ import time
165
+
166
+ # Update call count
167
+ self.call_counts[tool_name] = self.call_counts.get(tool_name, 0) + 1
168
+
169
+ # Create call record
170
+ call = MockCall(
171
+ tool_name=tool_name,
172
+ args=args,
173
+ result=result,
174
+ call_number=self.call_counts[tool_name],
175
+ timestamp=time.time(),
176
+ )
177
+
178
+ # Add to history
179
+ if tool_name not in self.call_history:
180
+ self.call_history[tool_name] = []
181
+ self.call_history[tool_name].append(call)
182
+
183
+ logger.debug(f"Recorded call to '{tool_name}' (call #{call.call_number})")
184
+
185
+ def get_call_count(self, tool_name: str) -> int:
186
+ """
187
+ Get the number of times a tool was called.
188
+
189
+ Args:
190
+ tool_name: Name of the tool
191
+
192
+ Returns:
193
+ Number of calls to the tool
194
+ """
195
+ return self.call_counts.get(tool_name, 0)
196
+
197
+ def get_call_history(self, tool_name: str) -> List[MockCall]:
198
+ """
199
+ Get the call history for a tool.
200
+
201
+ Args:
202
+ tool_name: Name of the tool
203
+
204
+ Returns:
205
+ List of MockCall records
206
+ """
207
+ return self.call_history.get(tool_name, [])
208
+
209
+ def reset(self) -> None:
210
+ """Reset all mock state."""
211
+ self.call_history.clear()
212
+ self.call_counts.clear()
213
+ logger.debug("Mock manager state reset")
214
+
215
+ def _matches_condition(self, args: Dict[str, Any], condition: Dict[str, Any]) -> bool:
216
+ """
217
+ Check if args match a condition.
218
+
219
+ Args:
220
+ args: Tool arguments to check
221
+ condition: Condition dict with patterns to match
222
+
223
+ Returns:
224
+ True if condition matches
225
+ """
226
+ for key, pattern in condition.items():
227
+ if key not in args:
228
+ return False
229
+
230
+ arg_value = args[key]
231
+
232
+ # Handle different pattern types
233
+ if isinstance(pattern, str):
234
+ # Check for operators
235
+ if pattern.startswith("contains:"):
236
+ substring = pattern[9:].strip()
237
+ if substring not in str(arg_value):
238
+ return False
239
+ elif pattern.startswith("startswith:"):
240
+ prefix = pattern[11:].strip()
241
+ if not str(arg_value).startswith(prefix):
242
+ return False
243
+ elif pattern.startswith("endswith:"):
244
+ suffix = pattern[9:].strip()
245
+ if not str(arg_value).endswith(suffix):
246
+ return False
247
+ else:
248
+ # Exact match
249
+ if arg_value != pattern:
250
+ return False
251
+ else:
252
+ # Direct comparison
253
+ if arg_value != pattern:
254
+ return False
255
+
256
+ return True
257
+
258
+ def enable_mock(self, tool_name: Optional[str] = None) -> None:
259
+ """
260
+ Enable mocking for a specific tool or all tools.
261
+
262
+ Args:
263
+ tool_name: Specific tool to enable, or None for all
264
+ """
265
+ if tool_name:
266
+ if tool_name in self.mocks:
267
+ self.mocks[tool_name].enabled = True
268
+ logger.info(f"Enabled mock for tool '{tool_name}'")
269
+ else:
270
+ self.enabled = True
271
+ logger.info("Enabled all mocks")
272
+
273
+ def disable_mock(self, tool_name: Optional[str] = None) -> None:
274
+ """
275
+ Disable mocking for a specific tool or all tools.
276
+
277
+ Args:
278
+ tool_name: Specific tool to disable, or None for all
279
+ """
280
+ if tool_name:
281
+ if tool_name in self.mocks:
282
+ self.mocks[tool_name].enabled = False
283
+ logger.info(f"Disabled mock for tool '{tool_name}'")
284
+ else:
285
+ self.enabled = False
286
+ logger.info("Disabled all mocks")