tactus 0.33.0__py3-none-any.whl → 0.34.1__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 +1 -1
- tactus/adapters/__init__.py +18 -1
- tactus/adapters/broker_log.py +127 -34
- tactus/adapters/channels/__init__.py +153 -0
- tactus/adapters/channels/base.py +174 -0
- tactus/adapters/channels/broker.py +179 -0
- tactus/adapters/channels/cli.py +448 -0
- tactus/adapters/channels/host.py +225 -0
- tactus/adapters/channels/ipc.py +297 -0
- tactus/adapters/channels/sse.py +305 -0
- tactus/adapters/cli_hitl.py +223 -1
- tactus/adapters/control_loop.py +879 -0
- tactus/adapters/file_storage.py +35 -2
- tactus/adapters/ide_log.py +7 -1
- tactus/backends/http_backend.py +0 -1
- tactus/broker/client.py +31 -1
- tactus/broker/server.py +416 -92
- tactus/cli/app.py +270 -7
- tactus/cli/control.py +393 -0
- tactus/core/config_manager.py +33 -6
- tactus/core/dsl_stubs.py +102 -18
- tactus/core/execution_context.py +265 -8
- tactus/core/lua_sandbox.py +8 -9
- tactus/core/registry.py +19 -2
- tactus/core/runtime.py +235 -27
- tactus/docker/Dockerfile.pypi +49 -0
- tactus/docs/__init__.py +33 -0
- tactus/docs/extractor.py +326 -0
- tactus/docs/html_renderer.py +72 -0
- tactus/docs/models.py +121 -0
- tactus/docs/templates/base.html +204 -0
- tactus/docs/templates/index.html +58 -0
- tactus/docs/templates/module.html +96 -0
- tactus/dspy/agent.py +382 -22
- tactus/dspy/broker_lm.py +57 -6
- tactus/dspy/config.py +14 -3
- tactus/dspy/history.py +2 -1
- tactus/dspy/module.py +136 -11
- tactus/dspy/signature.py +0 -1
- tactus/ide/server.py +300 -9
- tactus/primitives/human.py +619 -47
- tactus/primitives/system.py +0 -1
- tactus/protocols/__init__.py +25 -0
- tactus/protocols/control.py +427 -0
- tactus/protocols/notification.py +207 -0
- tactus/sandbox/container_runner.py +79 -11
- tactus/sandbox/docker_manager.py +23 -0
- tactus/sandbox/entrypoint.py +26 -0
- tactus/sandbox/protocol.py +3 -0
- tactus/stdlib/README.md +77 -0
- tactus/stdlib/__init__.py +27 -1
- tactus/stdlib/classify/__init__.py +165 -0
- tactus/stdlib/classify/classify.spec.tac +195 -0
- tactus/stdlib/classify/classify.tac +257 -0
- tactus/stdlib/classify/fuzzy.py +282 -0
- tactus/stdlib/classify/llm.py +319 -0
- tactus/stdlib/classify/primitive.py +287 -0
- tactus/stdlib/core/__init__.py +57 -0
- tactus/stdlib/core/base.py +320 -0
- tactus/stdlib/core/confidence.py +211 -0
- tactus/stdlib/core/models.py +161 -0
- tactus/stdlib/core/retry.py +171 -0
- tactus/stdlib/core/validation.py +274 -0
- tactus/stdlib/extract/__init__.py +125 -0
- tactus/stdlib/extract/llm.py +330 -0
- tactus/stdlib/extract/primitive.py +256 -0
- tactus/stdlib/tac/tactus/classify/base.tac +51 -0
- tactus/stdlib/tac/tactus/classify/fuzzy.tac +87 -0
- tactus/stdlib/tac/tactus/classify/index.md +77 -0
- tactus/stdlib/tac/tactus/classify/init.tac +29 -0
- tactus/stdlib/tac/tactus/classify/llm.tac +150 -0
- tactus/stdlib/tac/tactus/classify.spec.tac +191 -0
- tactus/stdlib/tac/tactus/extract/base.tac +138 -0
- tactus/stdlib/tac/tactus/extract/index.md +96 -0
- tactus/stdlib/tac/tactus/extract/init.tac +27 -0
- tactus/stdlib/tac/tactus/extract/llm.tac +201 -0
- tactus/stdlib/tac/tactus/extract.spec.tac +153 -0
- tactus/stdlib/tac/tactus/generate/base.tac +142 -0
- tactus/stdlib/tac/tactus/generate/index.md +195 -0
- tactus/stdlib/tac/tactus/generate/init.tac +28 -0
- tactus/stdlib/tac/tactus/generate/llm.tac +169 -0
- tactus/stdlib/tac/tactus/generate.spec.tac +210 -0
- tactus/testing/behave_integration.py +171 -7
- tactus/testing/context.py +0 -1
- tactus/testing/evaluation_runner.py +0 -1
- tactus/testing/gherkin_parser.py +0 -1
- tactus/testing/mock_hitl.py +0 -1
- tactus/testing/mock_tools.py +0 -1
- tactus/testing/models.py +0 -1
- tactus/testing/steps/builtin.py +0 -1
- tactus/testing/steps/custom.py +81 -22
- tactus/testing/steps/registry.py +0 -1
- tactus/testing/test_runner.py +7 -1
- tactus/validation/semantic_visitor.py +11 -5
- tactus/validation/validator.py +0 -1
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/METADATA +14 -2
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/RECORD +100 -49
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/WHEEL +0 -0
- {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.33.0.dist-info → tactus-0.34.1.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
|
|
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
|
|
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
tactus/testing/gherkin_parser.py
CHANGED
tactus/testing/mock_hitl.py
CHANGED
tactus/testing/mock_tools.py
CHANGED
tactus/testing/models.py
CHANGED
tactus/testing/steps/builtin.py
CHANGED
tactus/testing/steps/custom.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
38
|
+
pattern: The step text pattern (regex with capture groups)
|
|
30
39
|
lua_function: Lua function reference to execute
|
|
31
40
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
77
|
+
result = self._match(step_text)
|
|
78
|
+
if result:
|
|
79
|
+
lua_func, groups = result
|
|
48
80
|
try:
|
|
49
|
-
# Call Lua function with context
|
|
50
|
-
|
|
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
|
|
61
|
-
return step_text
|
|
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
|
|
64
|
-
"""
|
|
65
|
-
|
|
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.
|
|
127
|
+
self._steps.clear()
|
|
128
|
+
self._patterns.clear()
|
tactus/testing/steps/registry.py
CHANGED
tactus/testing/test_runner.py
CHANGED
|
@@ -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
|
-
#
|
|
471
|
-
# - Specification([[ Gherkin text ]]) (
|
|
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
|
-
|
|
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 []
|
tactus/validation/validator.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tactus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.34.1
|
|
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:
|
|
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`:
|