tactus 0.33.0__py3-none-any.whl → 0.34.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 (100) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/__init__.py +18 -1
  3. tactus/adapters/broker_log.py +127 -34
  4. tactus/adapters/channels/__init__.py +153 -0
  5. tactus/adapters/channels/base.py +174 -0
  6. tactus/adapters/channels/broker.py +179 -0
  7. tactus/adapters/channels/cli.py +448 -0
  8. tactus/adapters/channels/host.py +225 -0
  9. tactus/adapters/channels/ipc.py +297 -0
  10. tactus/adapters/channels/sse.py +305 -0
  11. tactus/adapters/cli_hitl.py +223 -1
  12. tactus/adapters/control_loop.py +879 -0
  13. tactus/adapters/file_storage.py +35 -2
  14. tactus/adapters/ide_log.py +7 -1
  15. tactus/backends/http_backend.py +0 -1
  16. tactus/broker/client.py +31 -1
  17. tactus/broker/server.py +416 -92
  18. tactus/cli/app.py +270 -7
  19. tactus/cli/control.py +393 -0
  20. tactus/core/config_manager.py +33 -6
  21. tactus/core/dsl_stubs.py +102 -18
  22. tactus/core/execution_context.py +265 -8
  23. tactus/core/lua_sandbox.py +8 -9
  24. tactus/core/registry.py +19 -2
  25. tactus/core/runtime.py +235 -27
  26. tactus/docker/Dockerfile.pypi +49 -0
  27. tactus/docs/__init__.py +33 -0
  28. tactus/docs/extractor.py +326 -0
  29. tactus/docs/html_renderer.py +72 -0
  30. tactus/docs/models.py +121 -0
  31. tactus/docs/templates/base.html +204 -0
  32. tactus/docs/templates/index.html +58 -0
  33. tactus/docs/templates/module.html +96 -0
  34. tactus/dspy/agent.py +382 -22
  35. tactus/dspy/broker_lm.py +57 -6
  36. tactus/dspy/config.py +14 -3
  37. tactus/dspy/history.py +2 -1
  38. tactus/dspy/module.py +136 -11
  39. tactus/dspy/signature.py +0 -1
  40. tactus/ide/server.py +300 -9
  41. tactus/primitives/human.py +619 -47
  42. tactus/primitives/system.py +0 -1
  43. tactus/protocols/__init__.py +25 -0
  44. tactus/protocols/control.py +427 -0
  45. tactus/protocols/notification.py +207 -0
  46. tactus/sandbox/container_runner.py +79 -11
  47. tactus/sandbox/docker_manager.py +23 -0
  48. tactus/sandbox/entrypoint.py +26 -0
  49. tactus/sandbox/protocol.py +3 -0
  50. tactus/stdlib/README.md +77 -0
  51. tactus/stdlib/__init__.py +27 -1
  52. tactus/stdlib/classify/__init__.py +165 -0
  53. tactus/stdlib/classify/classify.spec.tac +195 -0
  54. tactus/stdlib/classify/classify.tac +257 -0
  55. tactus/stdlib/classify/fuzzy.py +282 -0
  56. tactus/stdlib/classify/llm.py +319 -0
  57. tactus/stdlib/classify/primitive.py +287 -0
  58. tactus/stdlib/core/__init__.py +57 -0
  59. tactus/stdlib/core/base.py +320 -0
  60. tactus/stdlib/core/confidence.py +211 -0
  61. tactus/stdlib/core/models.py +161 -0
  62. tactus/stdlib/core/retry.py +171 -0
  63. tactus/stdlib/core/validation.py +274 -0
  64. tactus/stdlib/extract/__init__.py +125 -0
  65. tactus/stdlib/extract/llm.py +330 -0
  66. tactus/stdlib/extract/primitive.py +256 -0
  67. tactus/stdlib/tac/tactus/classify/base.tac +51 -0
  68. tactus/stdlib/tac/tactus/classify/fuzzy.tac +87 -0
  69. tactus/stdlib/tac/tactus/classify/index.md +77 -0
  70. tactus/stdlib/tac/tactus/classify/init.tac +29 -0
  71. tactus/stdlib/tac/tactus/classify/llm.tac +150 -0
  72. tactus/stdlib/tac/tactus/classify.spec.tac +191 -0
  73. tactus/stdlib/tac/tactus/extract/base.tac +138 -0
  74. tactus/stdlib/tac/tactus/extract/index.md +96 -0
  75. tactus/stdlib/tac/tactus/extract/init.tac +27 -0
  76. tactus/stdlib/tac/tactus/extract/llm.tac +201 -0
  77. tactus/stdlib/tac/tactus/extract.spec.tac +153 -0
  78. tactus/stdlib/tac/tactus/generate/base.tac +142 -0
  79. tactus/stdlib/tac/tactus/generate/index.md +195 -0
  80. tactus/stdlib/tac/tactus/generate/init.tac +28 -0
  81. tactus/stdlib/tac/tactus/generate/llm.tac +169 -0
  82. tactus/stdlib/tac/tactus/generate.spec.tac +210 -0
  83. tactus/testing/behave_integration.py +171 -7
  84. tactus/testing/context.py +0 -1
  85. tactus/testing/evaluation_runner.py +0 -1
  86. tactus/testing/gherkin_parser.py +0 -1
  87. tactus/testing/mock_hitl.py +0 -1
  88. tactus/testing/mock_tools.py +0 -1
  89. tactus/testing/models.py +0 -1
  90. tactus/testing/steps/builtin.py +0 -1
  91. tactus/testing/steps/custom.py +81 -22
  92. tactus/testing/steps/registry.py +0 -1
  93. tactus/testing/test_runner.py +7 -1
  94. tactus/validation/semantic_visitor.py +11 -5
  95. tactus/validation/validator.py +0 -1
  96. {tactus-0.33.0.dist-info → tactus-0.34.0.dist-info}/METADATA +14 -2
  97. {tactus-0.33.0.dist-info → tactus-0.34.0.dist-info}/RECORD +100 -49
  98. {tactus-0.33.0.dist-info → tactus-0.34.0.dist-info}/WHEEL +0 -0
  99. {tactus-0.33.0.dist-info → tactus-0.34.0.dist-info}/entry_points.txt +0 -0
  100. {tactus-0.33.0.dist-info → tactus-0.34.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,16 +8,145 @@ from parsed Gherkin and registered steps.
8
8
  import logging
9
9
  import tempfile
10
10
  from pathlib import Path
11
- from typing import Dict, List, Optional
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
13
  from .models import ParsedFeature, ParsedScenario
14
14
  from .steps.registry import StepRegistry
15
15
  from .steps.custom import CustomStepManager
16
16
 
17
-
18
17
  logger = logging.getLogger(__name__)
19
18
 
20
19
 
20
+ def load_custom_steps_from_lua(procedure_file: Path) -> Dict[str, Any]:
21
+ """
22
+ Load custom step definitions from a procedure file using the Lua runtime.
23
+
24
+ This function executes the Lua code to capture actual Lua function references,
25
+ unlike the validator which does static analysis and returns None for functions.
26
+
27
+ Args:
28
+ procedure_file: Path to the .tac procedure file
29
+
30
+ Returns:
31
+ Dict mapping step patterns to Lua function references
32
+ """
33
+ from tactus.core.lua_sandbox import LuaSandbox
34
+ from tactus.core.registry import RegistryBuilder
35
+ from tactus.core.dsl_stubs import create_dsl_stubs
36
+
37
+ # Create a minimal sandbox and builder
38
+ sandbox = LuaSandbox()
39
+ builder = RegistryBuilder()
40
+
41
+ # Create DSL stubs that capture the Lua functions
42
+ stubs = create_dsl_stubs(builder, tool_primitive=None, mock_manager=None)
43
+
44
+ # Remove internal items
45
+ stubs.pop("_registries", None)
46
+ stubs.pop("_tactus_register_binding", None)
47
+
48
+ # Inject stubs into sandbox
49
+ for name, stub in stubs.items():
50
+ sandbox.set_global(name, stub)
51
+
52
+ # Execute the procedure file
53
+ source = procedure_file.read_text()
54
+ try:
55
+ sandbox.execute(source)
56
+ except Exception as e:
57
+ logger.warning(f"Error executing procedure for custom steps: {e}")
58
+ return {}
59
+
60
+ # Return the custom steps with actual Lua function references
61
+ result = builder.validate()
62
+ if result.registry and result.registry.custom_steps:
63
+ return result.registry.custom_steps
64
+ return {}
65
+
66
+
67
+ def load_custom_steps_in_context(test_context: Any) -> Dict[str, Any]:
68
+ """
69
+ Load custom step definitions using the test context's runtime.
70
+
71
+ This ensures the Lua functions have access to a proper runtime for
72
+ executing agents, Classify primitives, etc.
73
+
74
+ Args:
75
+ test_context: TactusTestContext with an initialized runtime
76
+
77
+ Returns:
78
+ Dict mapping step patterns to Lua function references
79
+ """
80
+ from tactus.core.registry import RegistryBuilder
81
+ from tactus.core.dsl_stubs import create_dsl_stubs
82
+
83
+ # Ensure runtime is set up
84
+ if not test_context.runtime:
85
+ test_context.setup_runtime()
86
+
87
+ # Get the runtime's sandbox
88
+ runtime = test_context.runtime
89
+
90
+ # Create a new builder to capture custom steps
91
+ builder = RegistryBuilder()
92
+
93
+ # Ensure a mock manager exists so Mocks {} in spec files can be applied
94
+ # during custom step execution, even when tests are not in mocked mode.
95
+ mock_manager = runtime.mock_manager if hasattr(runtime, "mock_manager") else None
96
+ if mock_manager is None:
97
+ from tactus.core.mocking import MockManager
98
+
99
+ mock_manager = MockManager()
100
+
101
+ # Create DSL stubs connected to the runtime
102
+ # We pass the runtime's existing components including execution_context
103
+ stubs = create_dsl_stubs(
104
+ builder,
105
+ tool_primitive=runtime.tool_primitive if hasattr(runtime, "tool_primitive") else None,
106
+ mock_manager=mock_manager,
107
+ runtime_context={
108
+ "runtime": runtime,
109
+ "execution_context": (
110
+ runtime.execution_context if hasattr(runtime, "execution_context") else None
111
+ ),
112
+ "registry": runtime.registry if hasattr(runtime, "registry") else None,
113
+ "log_handler": runtime.log_handler if hasattr(runtime, "log_handler") else None,
114
+ "mock_manager": mock_manager,
115
+ "tool_primitive": (
116
+ runtime.tool_primitive if hasattr(runtime, "tool_primitive") else None
117
+ ),
118
+ "_created_agents": {},
119
+ },
120
+ )
121
+
122
+ # Remove internal items
123
+ stubs.pop("_registries", None)
124
+ stubs.pop("_tactus_register_binding", None)
125
+
126
+ # Create a fresh sandbox for loading custom steps
127
+ from tactus.core.lua_sandbox import LuaSandbox
128
+
129
+ sandbox = LuaSandbox()
130
+
131
+ # Inject stubs into sandbox
132
+ for name, stub in stubs.items():
133
+ sandbox.set_global(name, stub)
134
+
135
+ # Execute the procedure file to capture step definitions
136
+ source = test_context.procedure_file.read_text()
137
+ try:
138
+ sandbox.execute(source)
139
+ except Exception as e:
140
+ logger.warning(f"Error loading custom steps in context: {e}")
141
+ return {}
142
+
143
+ # Return the custom steps
144
+ result = builder.validate()
145
+ if result.registry and result.registry.custom_steps:
146
+ return result.registry.custom_steps
147
+ return {}
148
+
149
+
21
150
  class BehaveFeatureGenerator:
22
151
  """
23
152
  Generates Behave-compatible .feature files from parsed Gherkin.
@@ -173,6 +302,32 @@ class BehaveStepsGenerator:
173
302
  f.write(" # Call the actual step function from builtin module\n")
174
303
  f.write(f" builtin.{func_name}(context.tac, **kwargs)\n\n")
175
304
 
305
+ # Generate custom step patterns using regex matcher
306
+ custom_patterns = custom_steps.get_all_patterns() if custom_steps else []
307
+ if custom_patterns:
308
+ f.write("# Custom step definitions from procedure file\n")
309
+ f.write("use_step_matcher('re')\n\n")
310
+
311
+ for i, pattern in enumerate(custom_patterns):
312
+ wrapper_name = f"custom_step_{i}"
313
+ # Escape the pattern for Python string (use raw string)
314
+ escaped_pattern = pattern.replace("\\", "\\\\").replace("'", "\\'")
315
+ # For the pattern argument, just escape single quotes since we use single-quoted raw string
316
+ pattern_arg = pattern.replace("'", "\\'")
317
+ # Escape docstring (remove quotes for safety)
318
+ docstring_pattern = pattern[:50].replace('"', "'").replace("\\", "")
319
+
320
+ f.write(f"@step(r'{escaped_pattern}')\n")
321
+ f.write(f"def {wrapper_name}(context, *args):\n")
322
+ f.write(f' """Custom step: {docstring_pattern}"""\n')
323
+ f.write(" # Execute via custom step manager with captured groups\n")
324
+ f.write(
325
+ f" context.custom_steps.execute_by_pattern(r'{pattern_arg}', context.tac, *args)\n\n"
326
+ )
327
+
328
+ # Switch back to parse matcher for any remaining steps
329
+ f.write("use_step_matcher('parse')\n\n")
330
+
176
331
  logger.info(f"Generated steps file: {steps_file}")
177
332
  return steps_file
178
333
 
@@ -267,7 +422,10 @@ class BehaveEnvironmentGenerator:
267
422
  f.write("from tactus.testing.context import TactusTestContext\n")
268
423
  f.write("from tactus.testing.steps.registry import StepRegistry\n")
269
424
  f.write("from tactus.testing.steps.builtin import register_builtin_steps\n")
270
- f.write("from tactus.testing.steps.custom import CustomStepManager\n\n")
425
+ f.write("from tactus.testing.steps.custom import CustomStepManager\n")
426
+ f.write(
427
+ "from tactus.testing.behave_integration import load_custom_steps_in_context\n\n"
428
+ )
271
429
 
272
430
  f.write("def before_all(context):\n")
273
431
  f.write(' """Setup before all tests."""\n')
@@ -275,9 +433,6 @@ class BehaveEnvironmentGenerator:
275
433
  f.write(" context.step_registry = StepRegistry()\n")
276
434
  f.write(" register_builtin_steps(context.step_registry)\n")
277
435
  f.write(" \n")
278
- f.write(" # Initialize custom step manager\n")
279
- f.write(" context.custom_steps = CustomStepManager()\n")
280
- f.write(" \n")
281
436
  f.write(" # Store test configuration (using absolute path)\n")
282
437
  f.write(f" context.procedure_file = Path(r'{absolute_procedure_file}')\n")
283
438
  f.write(f" context.mock_tools = json.loads('{mock_tools_json}')\n")
@@ -308,7 +463,16 @@ class BehaveEnvironmentGenerator:
308
463
  " context.mock_registry = UnifiedMockRegistry(hitl_handler=MockHITLHandler())\n"
309
464
  )
310
465
  f.write(" # Share mock registry with TactusTestContext\n")
311
- f.write(" context.tac.mock_registry = context.mock_registry\n\n")
466
+ f.write(" context.tac.mock_registry = context.mock_registry\n")
467
+ f.write(" \n")
468
+ f.write(" # Load custom steps with runtime context for this scenario\n")
469
+ f.write(
470
+ " # (This ensures Lua functions have access to the runtime for agents, etc.)\n"
471
+ )
472
+ f.write(" context.custom_steps = CustomStepManager()\n")
473
+ f.write(" custom_steps_dict = load_custom_steps_in_context(context.tac)\n")
474
+ f.write(" for pattern, lua_func in custom_steps_dict.items():\n")
475
+ f.write(" context.custom_steps.register_from_lua(pattern, lua_func)\n\n")
312
476
 
313
477
  f.write("def after_scenario(context, scenario):\n")
314
478
  f.write(' """Cleanup after each scenario."""\n')
tactus/testing/context.py CHANGED
@@ -10,7 +10,6 @@ import logging
10
10
  from pathlib import Path
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
-
14
13
  logger = logging.getLogger(__name__)
15
14
 
16
15
 
@@ -14,7 +14,6 @@ from typing import List
14
14
  from .models import ScenarioResult, EvaluationResult
15
15
  from .test_runner import TactusTestRunner
16
16
 
17
-
18
17
  logger = logging.getLogger(__name__)
19
18
 
20
19
 
@@ -15,7 +15,6 @@ except ImportError:
15
15
 
16
16
  from .models import ParsedStep, ParsedScenario, ParsedFeature
17
17
 
18
-
19
18
  logger = logging.getLogger(__name__)
20
19
 
21
20
 
@@ -11,7 +11,6 @@ from typing import Any, Dict, Optional
11
11
 
12
12
  from tactus.protocols.models import HITLRequest, HITLResponse
13
13
 
14
-
15
14
  logger = logging.getLogger(__name__)
16
15
 
17
16
 
@@ -10,7 +10,6 @@ from typing import Any, Dict
10
10
 
11
11
  from tactus.primitives.tool import ToolPrimitive, ToolCall
12
12
 
13
-
14
13
  logger = logging.getLogger(__name__)
15
14
 
16
15
 
tactus/testing/models.py CHANGED
@@ -6,7 +6,6 @@ from datetime import datetime
6
6
  from typing import List, Optional
7
7
  from pydantic import BaseModel, Field
8
8
 
9
-
10
9
  # Parsed Gherkin Models (from gherkin-official)
11
10
 
12
11
 
@@ -18,7 +18,6 @@ from typing import Any
18
18
 
19
19
  from .registry import StepRegistry
20
20
 
21
-
22
21
  logger = logging.getLogger(__name__)
23
22
 
24
23
 
@@ -1,40 +1,71 @@
1
1
  """
2
2
  Custom step manager for user-defined Lua step functions.
3
+
4
+ Supports regex pattern matching for step definitions, allowing
5
+ expressive BDD specifications with captured arguments.
3
6
  """
4
7
 
5
8
  import logging
6
- from typing import Any, Dict
7
-
9
+ import re
10
+ from typing import Any, Dict, Optional, Tuple
8
11
 
9
12
  logger = logging.getLogger(__name__)
10
13
 
11
14
 
12
15
  class CustomStepManager:
13
16
  """
14
- Manages custom Lua step definitions.
17
+ Manages custom Lua step definitions with regex pattern matching.
15
18
 
16
19
  Allows users to define custom steps in their procedure files
17
- using the step() function with Lua implementations.
20
+ using the Step() function with regex patterns and Lua implementations.
21
+
22
+ Example:
23
+ Step("a classifier with classes (.+)", function(ctx, classes)
24
+ -- classes contains the captured group
25
+ end)
18
26
  """
19
27
 
20
28
  def __init__(self, lua_sandbox=None):
21
29
  self.lua_sandbox = lua_sandbox
22
- self.custom_steps: Dict[str, Any] = {}
30
+ self._steps: Dict[re.Pattern, Any] = {} # compiled_pattern -> lua_function
31
+ self._patterns: Dict[str, re.Pattern] = {} # pattern_str -> compiled_pattern
23
32
 
24
- def register_from_lua(self, step_text: str, lua_function: Any) -> None:
33
+ def register_from_lua(self, pattern: str, lua_function: Any) -> None:
25
34
  """
26
- Register a custom step from Lua code.
35
+ Register a custom step from Lua code with regex pattern.
27
36
 
28
37
  Args:
29
- step_text: The step text pattern (exact match)
38
+ pattern: The step text pattern (regex with capture groups)
30
39
  lua_function: Lua function reference to execute
31
40
  """
32
- self.custom_steps[step_text] = lua_function
33
- logger.debug(f"Registered custom step: {step_text}")
41
+ try:
42
+ compiled = re.compile(pattern, re.IGNORECASE)
43
+ self._steps[compiled] = lua_function
44
+ self._patterns[pattern] = compiled
45
+ logger.debug(f"Registered custom step pattern: {pattern}")
46
+ except re.error as e:
47
+ logger.error(f"Invalid regex pattern '{pattern}': {e}")
48
+ raise ValueError(f"Invalid step pattern: {e}")
49
+
50
+ def _match(self, step_text: str) -> Optional[Tuple[Any, tuple]]:
51
+ """
52
+ Find a matching pattern and return the Lua function with captured groups.
53
+
54
+ Args:
55
+ step_text: The step text to match
56
+
57
+ Returns:
58
+ Tuple of (lua_function, captured_groups) if match found, None otherwise
59
+ """
60
+ for pattern, lua_func in self._steps.items():
61
+ match = pattern.match(step_text)
62
+ if match:
63
+ return lua_func, match.groups()
64
+ return None
34
65
 
35
66
  def execute(self, step_text: str, context: Any) -> bool:
36
67
  """
37
- Execute custom Lua step if it exists.
68
+ Execute custom Lua step if pattern matches.
38
69
 
39
70
  Args:
40
71
  step_text: The step text to match
@@ -43,12 +74,12 @@ class CustomStepManager:
43
74
  Returns:
44
75
  True if step was found and executed, False otherwise
45
76
  """
46
- if step_text in self.custom_steps:
47
- lua_func = self.custom_steps[step_text]
77
+ result = self._match(step_text)
78
+ if result:
79
+ lua_func, groups = result
48
80
  try:
49
- # Call Lua function with context
50
- # The Lua function should perform assertions
51
- lua_func(context)
81
+ # Call Lua function with context + captured groups
82
+ lua_func(context, *groups)
52
83
  return True
53
84
  except Exception as e:
54
85
  logger.error(f"Custom step '{step_text}' failed: {e}")
@@ -57,13 +88,41 @@ class CustomStepManager:
57
88
  return False
58
89
 
59
90
  def has_step(self, step_text: str) -> bool:
60
- """Check if custom step exists."""
61
- return step_text in self.custom_steps
91
+ """Check if any pattern matches the step text."""
92
+ return self._match(step_text) is not None
93
+
94
+ def get_all_patterns(self) -> list[str]:
95
+ """Get all registered pattern strings."""
96
+ return list(self._patterns.keys())
62
97
 
63
- def get_all_steps(self) -> list[str]:
64
- """Get all registered custom step texts."""
65
- return list(self.custom_steps.keys())
98
+ def execute_by_pattern(self, pattern: str, context: Any, *args) -> bool:
99
+ """
100
+ Execute custom Lua step by pattern string with pre-captured args.
101
+
102
+ This is used when the regex matching has already been done (e.g., by Behave)
103
+ and we just need to call the Lua function with the captured groups.
104
+
105
+ Args:
106
+ pattern: The pattern string to look up
107
+ context: Test context object
108
+ *args: Captured groups from regex match
109
+
110
+ Returns:
111
+ True if step was found and executed, False otherwise
112
+ """
113
+ if pattern in self._patterns:
114
+ compiled = self._patterns[pattern]
115
+ lua_func = self._steps.get(compiled)
116
+ if lua_func:
117
+ try:
118
+ lua_func(context, *args)
119
+ return True
120
+ except Exception as e:
121
+ logger.error(f"Custom step '{pattern}' failed: {e}")
122
+ raise AssertionError(f"Custom step failed: {e}")
123
+ return False
66
124
 
67
125
  def clear(self) -> None:
68
126
  """Clear all custom steps."""
69
- self.custom_steps.clear()
127
+ self._steps.clear()
128
+ self._patterns.clear()
@@ -6,7 +6,6 @@ import re
6
6
  import logging
7
7
  from typing import Callable, Dict, Optional, Pattern, Tuple
8
8
 
9
-
10
9
  logger = logging.getLogger(__name__)
11
10
 
12
11
 
@@ -68,13 +68,19 @@ class TactusTestRunner:
68
68
  # Register built-in steps
69
69
  register_builtin_steps(self.step_registry)
70
70
 
71
- def setup(self, gherkin_text: str) -> None:
71
+ def setup(self, gherkin_text: str, custom_steps_dict: Optional[dict] = None) -> None:
72
72
  """
73
73
  Setup test environment from Gherkin text.
74
74
 
75
75
  Args:
76
76
  gherkin_text: Raw Gherkin feature text
77
+ custom_steps_dict: Optional dict of step patterns to Lua functions from registry
77
78
  """
79
+ # Register custom steps from registry if provided
80
+ if custom_steps_dict:
81
+ for pattern, lua_func in custom_steps_dict.items():
82
+ self.custom_steps.register_from_lua(pattern, lua_func)
83
+
78
84
  # Parse Gherkin
79
85
  parser = GherkinParser()
80
86
  self.parsed_feature = parser.parse(gherkin_text)
@@ -12,7 +12,6 @@ from .generated.LuaParser import LuaParser
12
12
  from .generated.LuaParserVisitor import LuaParserVisitor
13
13
  from tactus.core.registry import RegistryBuilder, ValidationMessage
14
14
 
15
-
16
15
  logger = logging.getLogger(__name__)
17
16
 
18
17
 
@@ -467,11 +466,18 @@ class TactusDSLVisitor(LuaParserVisitor):
467
466
  if args and len(args) >= 2:
468
467
  self.builder.register_hitl(args[0], args[1] if isinstance(args[1], dict) else {})
469
468
  elif func_name == "Specification": # CamelCase
470
- # Either:
471
- # - Specification([[ Gherkin text ]]) (alias for Specifications)
472
- # - Specification("name", { ... }) (structured form)
469
+ # Three supported forms:
470
+ # - Specification([[ Gherkin text ]]) (inline Gherkin)
471
+ # - Specification("name", { ... }) (structured form; legacy)
472
+ # - Specification { from = "path" } (external file reference)
473
473
  if args and len(args) == 1:
474
- self.builder.register_specifications(args[0])
474
+ arg = args[0]
475
+ if isinstance(arg, dict) and "from" in arg:
476
+ # External file reference
477
+ self.builder.register_specs_from(arg["from"])
478
+ else:
479
+ # Inline Gherkin text
480
+ self.builder.register_specifications(arg)
475
481
  elif args and len(args) >= 2:
476
482
  self.builder.register_specification(
477
483
  args[0], args[1] if isinstance(args[1], list) else []
@@ -18,7 +18,6 @@ from .semantic_visitor import TactusDSLVisitor
18
18
  from .error_listener import TactusErrorListener
19
19
  from tactus.core.registry import ValidationResult, ValidationMessage
20
20
 
21
-
22
21
  logger = logging.getLogger(__name__)
23
22
 
24
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tactus
3
- Version: 0.33.0
3
+ Version: 0.34.0
4
4
  Summary: Tactus: Lua-based DSL for agentic workflows
5
5
  Project-URL: Homepage, https://github.com/AnthusAI/Tactus
6
6
  Project-URL: Documentation, https://github.com/AnthusAI/Tactus/tree/main/docs
@@ -30,7 +30,7 @@ Requires-Dist: gherkin-official>=28.0.0
30
30
  Requires-Dist: h5py>=3.10
31
31
  Requires-Dist: lupa>=2.6
32
32
  Requires-Dist: nanoid>=2.0.0
33
- Requires-Dist: openai>=1.35.10
33
+ Requires-Dist: nest-asyncio>=1.5.0
34
34
  Requires-Dist: openpyxl>=3.1
35
35
  Requires-Dist: pyarrow>=14.0
36
36
  Requires-Dist: pydantic-ai[bedrock]
@@ -49,6 +49,16 @@ Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
49
49
  Requires-Dist: pytest>=8.0; extra == 'dev'
50
50
  Requires-Dist: python-semantic-release>=9.0.0; extra == 'dev'
51
51
  Requires-Dist: ruff; extra == 'dev'
52
+ Provides-Extra: discord
53
+ Requires-Dist: discord-py>=2.0; extra == 'discord'
54
+ Provides-Extra: email
55
+ Requires-Dist: aiosmtplib>=3.0; extra == 'email'
56
+ Provides-Extra: notifications
57
+ Requires-Dist: aiosmtplib>=3.0; extra == 'notifications'
58
+ Requires-Dist: discord-py>=2.0; extra == 'notifications'
59
+ Requires-Dist: slack-sdk>=3.0; extra == 'notifications'
60
+ Provides-Extra: slack
61
+ Requires-Dist: slack-sdk>=3.0; extra == 'slack'
52
62
  Description-Content-Type: text/markdown
53
63
 
54
64
  # Tactus
@@ -502,6 +512,8 @@ See [docs/TOOLS.md](docs/TOOLS.md) for the complete tools reference.
502
512
  pip install tactus
503
513
  ```
504
514
 
515
+ **Docker required by default:** `tactus run` uses a Docker sandbox for isolation and will error if Docker is not available. Use `--no-sandbox` (or set `sandbox.enabled: false` in config) to opt out when your architecture does not require container isolation.
516
+
505
517
  ### Your First Procedure
506
518
 
507
519
  Create `hello.tac`: