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.
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.2.dist-info/METADATA +1809 -0
  157. tactus-0.31.2.dist-info/RECORD +160 -0
  158. tactus-0.31.2.dist-info/WHEEL +4 -0
  159. tactus-0.31.2.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,380 @@
1
+ """
2
+ Behave integration layer for Tactus BDD testing.
3
+
4
+ Generates Behave-compatible .feature files and step definitions
5
+ from parsed Gherkin and registered steps.
6
+ """
7
+
8
+ import logging
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+ from .models import ParsedFeature, ParsedScenario
14
+ from .steps.registry import StepRegistry
15
+ from .steps.custom import CustomStepManager
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class BehaveFeatureGenerator:
22
+ """
23
+ Generates Behave-compatible .feature files from parsed Gherkin.
24
+ """
25
+
26
+ def generate(
27
+ self,
28
+ parsed_feature: ParsedFeature,
29
+ output_dir: Path,
30
+ filename: Optional[str] = None,
31
+ ) -> Path:
32
+ """
33
+ Generate .feature file from parsed Gherkin.
34
+
35
+ Args:
36
+ parsed_feature: Parsed Gherkin feature
37
+ output_dir: Directory to write feature file
38
+ filename: Optional custom filename (defaults to feature name)
39
+
40
+ Returns:
41
+ Path to generated feature file
42
+ """
43
+ if not filename:
44
+ # Sanitize feature name for filename
45
+ filename = parsed_feature.name.lower().replace(" ", "_") + ".feature"
46
+
47
+ feature_file = output_dir / filename
48
+
49
+ with open(feature_file, "w") as f:
50
+ # Write feature header
51
+ if parsed_feature.tags:
52
+ for tag in parsed_feature.tags:
53
+ f.write(f"@{tag}\n")
54
+
55
+ f.write(f"Feature: {parsed_feature.name}\n")
56
+
57
+ if parsed_feature.description:
58
+ # Indent description
59
+ for line in parsed_feature.description.split("\n"):
60
+ f.write(f" {line}\n")
61
+ f.write("\n")
62
+
63
+ # Write scenarios
64
+ for scenario in parsed_feature.scenarios:
65
+ self._write_scenario(f, scenario)
66
+
67
+ logger.info(f"Generated feature file: {feature_file}")
68
+ return feature_file
69
+
70
+ def _write_scenario(self, f, scenario: ParsedScenario) -> None:
71
+ """Write a scenario to the feature file."""
72
+ # Write scenario tags
73
+ if scenario.tags:
74
+ f.write(" ")
75
+ for tag in scenario.tags:
76
+ f.write(f"@{tag} ")
77
+ f.write("\n")
78
+
79
+ # Add tag for filtering by scenario name
80
+ # Remove special characters that could interfere with behave tags
81
+ import re
82
+
83
+ sanitized_name = re.sub(r"[^a-z0-9_]", "_", scenario.name.lower())
84
+ sanitized_name = re.sub(r"_+", "_", sanitized_name) # Collapse multiple underscores
85
+ f.write(f" @scenario_{sanitized_name}\n")
86
+
87
+ f.write(f" Scenario: {scenario.name}\n")
88
+
89
+ # Write steps
90
+ for step in scenario.steps:
91
+ f.write(f" {step.keyword} {step.message}\n")
92
+
93
+ f.write("\n")
94
+
95
+
96
+ class BehaveStepsGenerator:
97
+ """
98
+ Generates Python step definitions for Behave.
99
+ """
100
+
101
+ def generate(
102
+ self,
103
+ step_registry: StepRegistry,
104
+ custom_steps: CustomStepManager,
105
+ output_dir: Path,
106
+ ) -> Path:
107
+ """
108
+ Generate step_definitions.py for Behave.
109
+
110
+ Args:
111
+ step_registry: Registry of built-in steps
112
+ custom_steps: Manager for custom Lua steps
113
+ output_dir: Directory to write steps file
114
+
115
+ Returns:
116
+ Path to generated steps file
117
+ """
118
+ steps_dir = output_dir / "steps"
119
+ steps_dir.mkdir(exist_ok=True)
120
+
121
+ # Use unique filename based on output_dir to prevent conflicts
122
+ import hashlib
123
+
124
+ dir_hash = hashlib.md5(str(output_dir).encode()).hexdigest()[:8]
125
+ steps_file = steps_dir / f"tactus_steps_{dir_hash}.py"
126
+
127
+ with open(steps_file, "w") as f:
128
+ # Write imports
129
+ f.write("from behave import step, use_step_matcher\n")
130
+ f.write("import sys\n")
131
+ f.write("from pathlib import Path\n\n")
132
+
133
+ # Use parse matcher instead of regex
134
+ f.write("# Use parse matcher for simpler patterns\n")
135
+ f.write("use_step_matcher('parse')\n\n")
136
+
137
+ # Add tactus to path
138
+ f.write("# Add tactus to path\n")
139
+ f.write("sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))\n\n")
140
+
141
+ # Import all built-in step functions
142
+ f.write("from tactus.testing.steps import builtin\n")
143
+ f.write("from tactus.testing.context import TactusTestContext\n")
144
+ f.write("# Import mock steps for dependency mocking\n")
145
+ # Mock steps temporarily disabled due to pattern conflicts
146
+ # f.write("from tactus.testing.steps import mock_steps\n\n")
147
+
148
+ # Generate decorators for each built-in step pattern
149
+ # Map pattern to actual function (avoid duplicates)
150
+ pattern_to_func = {}
151
+ for pattern, func in step_registry._steps.items():
152
+ # Use pattern string as key to avoid duplicates
153
+ pattern_str = pattern.pattern
154
+ if pattern_str not in pattern_to_func:
155
+ pattern_to_func[pattern_str] = func.__name__
156
+
157
+ for pattern_str, func_name in pattern_to_func.items():
158
+ # Create a unique wrapper function name
159
+ wrapper_name = self._pattern_to_func_name(pattern_str)
160
+
161
+ # Convert regex pattern to parse pattern
162
+ # Replace (?P<name>...) with {name}
163
+ parse_pattern = self._regex_to_parse_pattern(pattern_str)
164
+
165
+ # Escape quotes in pattern for Python string
166
+ escaped_pattern = parse_pattern.replace("'", "\\'").replace('"', '\\"')
167
+
168
+ # Use @step() decorator which works for Given/When/Then/And/But
169
+ # This prevents duplicate step definition errors
170
+ f.write(f"@step('{escaped_pattern}')\n")
171
+ f.write(f"def {wrapper_name}(context, **kwargs):\n")
172
+ f.write(f' """Step: {escaped_pattern[:60]}"""\n')
173
+ f.write(" # Call the actual step function from builtin module\n")
174
+ f.write(f" builtin.{func_name}(context.tac, **kwargs)\n\n")
175
+
176
+ logger.info(f"Generated steps file: {steps_file}")
177
+ return steps_file
178
+
179
+ def _regex_to_parse_pattern(self, regex_pattern: str) -> str:
180
+ """Convert regex pattern to parse pattern."""
181
+ import re
182
+
183
+ # Replace (?P<name>\w+) with {name:w}
184
+ pattern = re.sub(r"\(\?P<(\w+)>\\w\+\)", r"{\1:w}", regex_pattern)
185
+ # Replace (?P<name>\d+) with {name:d}
186
+ pattern = re.sub(r"\(\?P<(\w+)>\\d\+\)", r"{\1:d}", pattern)
187
+ # Replace (?P<name>.+) with {name}
188
+ pattern = re.sub(r"\(\?P<(\w+)>\.\+\)", r"{\1}", pattern)
189
+ # Replace (?P<name>-?\d+\.?\d*) with {name} (numeric patterns)
190
+ # The backslashes in the original regex need to be matched literally
191
+ pattern = re.sub(r"\(\?P<(\w+)>-\?\\\\d\+\\\.\?\\\\d\*\)", r"{\1}", pattern)
192
+ # Also handle the simpler form without escapes in the source string
193
+ pattern = re.sub(r"\(\?P<(\w+)>-\?\\d\+\\\.\?\\d\*\)", r"{\1}", pattern)
194
+ # Catch-all for any remaining named groups with complex patterns
195
+ pattern = re.sub(r"\(\?P<(\w+)>[^)]+\)", r"{\1}", pattern)
196
+ # Convert regex escapes to literals: \[ -> [, \] -> ]
197
+ pattern = pattern.replace(r"\[", "[").replace(r"\]", "]")
198
+ return pattern
199
+
200
+ def _pattern_to_func_name(self, pattern: str) -> str:
201
+ """Convert regex pattern to valid Python function name."""
202
+ import re
203
+ import hashlib
204
+
205
+ # Remove regex special characters and convert to snake_case
206
+ name = re.sub(r"[^a-zA-Z0-9_]", "_", pattern)
207
+ name = re.sub(r"_+", "_", name) # Collapse multiple underscores
208
+ name = name.strip("_")
209
+ # Add hash to make unique across different temp directories
210
+ pattern_hash = hashlib.md5(pattern.encode()).hexdigest()[:8]
211
+ return f"step_{name[:40]}_{pattern_hash}" # Limit length + unique hash
212
+
213
+
214
+ class BehaveEnvironmentGenerator:
215
+ """
216
+ Generates environment.py for Behave with Tactus context setup.
217
+ """
218
+
219
+ def generate(
220
+ self,
221
+ output_dir: Path,
222
+ procedure_file: Path,
223
+ mock_tools: Optional[Dict] = None,
224
+ params: Optional[Dict] = None,
225
+ mcp_servers: Optional[Dict] = None,
226
+ tool_paths: Optional[List[str]] = None,
227
+ mocked: bool = False,
228
+ ) -> Path:
229
+ """
230
+ Generate environment.py for Behave.
231
+
232
+ Args:
233
+ output_dir: Directory to write environment file
234
+ procedure_file: Path to the procedure file being tested
235
+ mock_tools: Optional dict of tool_name -> mock_response
236
+ params: Optional dict of parameters to pass to procedure
237
+ mocked: Whether to use mocked dependencies
238
+
239
+ Returns:
240
+ Path to generated environment file
241
+ """
242
+ env_file = output_dir / "environment.py"
243
+
244
+ # Serialize mock_tools and params for embedding
245
+ import json
246
+
247
+ mock_tools_json = json.dumps(mock_tools or {}).replace("'", "\\'")
248
+ params_json = json.dumps(params or {}).replace("'", "\\'")
249
+ mcp_servers_json = json.dumps(mcp_servers or {}).replace("'", "\\'")
250
+ tool_paths_json = json.dumps(tool_paths or []).replace("'", "\\'")
251
+
252
+ # Convert procedure_file to absolute path so it works from temp behave directory
253
+ absolute_procedure_file = Path(procedure_file).resolve()
254
+
255
+ with open(env_file, "w") as f:
256
+ f.write('"""\n')
257
+ f.write("Behave environment for Tactus BDD testing.\n")
258
+ f.write('"""\n\n')
259
+
260
+ f.write("import sys\n")
261
+ f.write("import json\n")
262
+ f.write("from pathlib import Path\n\n")
263
+
264
+ f.write("# Add tactus to path\n")
265
+ f.write("sys.path.insert(0, str(Path(__file__).parent.parent.parent))\n\n")
266
+
267
+ f.write("from tactus.testing.context import TactusTestContext\n")
268
+ f.write("from tactus.testing.steps.registry import StepRegistry\n")
269
+ f.write("from tactus.testing.steps.builtin import register_builtin_steps\n")
270
+ f.write("from tactus.testing.steps.custom import CustomStepManager\n\n")
271
+
272
+ f.write("def before_all(context):\n")
273
+ f.write(' """Setup before all tests."""\n')
274
+ f.write(" # Initialize step registry\n")
275
+ f.write(" context.step_registry = StepRegistry()\n")
276
+ f.write(" register_builtin_steps(context.step_registry)\n")
277
+ f.write(" \n")
278
+ f.write(" # Initialize custom step manager\n")
279
+ f.write(" context.custom_steps = CustomStepManager()\n")
280
+ f.write(" \n")
281
+ f.write(" # Store test configuration (using absolute path)\n")
282
+ f.write(f" context.procedure_file = Path(r'{absolute_procedure_file}')\n")
283
+ f.write(f" context.mock_tools = json.loads('{mock_tools_json}')\n")
284
+ f.write(f" context.params = json.loads('{params_json}')\n")
285
+ f.write(f" context.mcp_servers = json.loads('{mcp_servers_json}')\n")
286
+ f.write(f" context.tool_paths = json.loads('{tool_paths_json}')\n")
287
+ f.write(f" context.mocked = {mocked}\n\n")
288
+
289
+ f.write("def before_scenario(context, scenario):\n")
290
+ f.write(' """Setup before each scenario."""\n')
291
+ f.write(" # Import mock registry for dependency mocking\n")
292
+ f.write(" from tactus.testing.mock_registry import UnifiedMockRegistry\n")
293
+ f.write(" from tactus.testing.mock_hitl import MockHITLHandler\n")
294
+ f.write(" \n")
295
+ f.write(" # Create fresh Tactus context for each scenario\n")
296
+ f.write(" context.tac = TactusTestContext(\n")
297
+ f.write(" procedure_file=context.procedure_file,\n")
298
+ f.write(" params=context.params,\n")
299
+ f.write(" mock_tools=context.mock_tools,\n")
300
+ f.write(" mcp_servers=context.mcp_servers,\n")
301
+ f.write(" tool_paths=context.tool_paths,\n")
302
+ f.write(" mocked=context.mocked,\n")
303
+ f.write(" )\n")
304
+ f.write(" \n")
305
+ f.write(" # Create mock registry for Gherkin steps to configure\n")
306
+ f.write(" if context.mocked:\n")
307
+ f.write(
308
+ " context.mock_registry = UnifiedMockRegistry(hitl_handler=MockHITLHandler())\n"
309
+ )
310
+ f.write(" # Share mock registry with TactusTestContext\n")
311
+ f.write(" context.tac.mock_registry = context.mock_registry\n\n")
312
+
313
+ f.write("def after_scenario(context, scenario):\n")
314
+ f.write(' """Cleanup after each scenario."""\n')
315
+ f.write(" # Attach execution metrics to scenario for reporting\n")
316
+ f.write(" if hasattr(context, 'tac') and context.tac:\n")
317
+ f.write(" scenario.total_cost = getattr(context.tac, 'total_cost', 0.0)\n")
318
+ f.write(" scenario.total_tokens = getattr(context.tac, 'total_tokens', 0)\n")
319
+ f.write(
320
+ " scenario.cost_breakdown = getattr(context.tac, 'cost_breakdown', [])\n"
321
+ )
322
+ f.write(" scenario.iterations = getattr(context.tac, 'iterations', 0)\n")
323
+ f.write(" scenario.tools_used = getattr(context.tac, 'tools_used', [])\n")
324
+ f.write(" # Cleanup runtime if it was created\n")
325
+ f.write(" if hasattr(context.tac, 'runtime') and context.tac.runtime:\n")
326
+ f.write(" # Any cleanup needed\n")
327
+ f.write(" pass\n")
328
+
329
+ logger.info(f"Generated environment file: {env_file}")
330
+ return env_file
331
+
332
+
333
+ def setup_behave_directory(
334
+ parsed_feature: ParsedFeature,
335
+ step_registry: StepRegistry,
336
+ custom_steps: CustomStepManager,
337
+ procedure_file: Path,
338
+ work_dir: Optional[Path] = None,
339
+ mock_tools: Optional[Dict] = None,
340
+ params: Optional[Dict] = None,
341
+ mcp_servers: Optional[Dict] = None,
342
+ tool_paths: Optional[List[str]] = None,
343
+ mocked: bool = False,
344
+ ) -> Path:
345
+ """
346
+ Setup complete Behave directory structure.
347
+
348
+ Args:
349
+ parsed_feature: Parsed Gherkin feature
350
+ step_registry: Registry of built-in steps
351
+ custom_steps: Custom step manager
352
+ procedure_file: Path to procedure file being tested
353
+ work_dir: Optional work directory (creates temp if not provided)
354
+ mock_tools: Optional dict of tool mocks
355
+ params: Optional dict of procedure parameters
356
+ mocked: Whether to use mocked dependencies
357
+
358
+ Returns:
359
+ Path to Behave work directory
360
+ """
361
+ if work_dir is None:
362
+ # Always create a unique temp directory
363
+ work_dir = Path(tempfile.mkdtemp(prefix="tactus_behave_"))
364
+ else:
365
+ work_dir.mkdir(parents=True, exist_ok=True)
366
+
367
+ # Generate feature file
368
+ feature_gen = BehaveFeatureGenerator()
369
+ feature_gen.generate(parsed_feature, work_dir)
370
+
371
+ # Generate step definitions
372
+ steps_gen = BehaveStepsGenerator()
373
+ steps_gen.generate(step_registry, custom_steps, work_dir)
374
+
375
+ # Generate environment.py with mock tools, params, and mocked flag
376
+ env_gen = BehaveEnvironmentGenerator()
377
+ env_gen.generate(work_dir, procedure_file, mock_tools, params, mcp_servers, tool_paths, mocked)
378
+
379
+ logger.info(f"Behave directory setup complete: {work_dir}")
380
+ return work_dir