tactus 0.34.0__py3-none-any.whl → 0.35.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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +15 -6
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/template_resolver.py
CHANGED
|
@@ -62,15 +62,15 @@ class TemplateResolver:
|
|
|
62
62
|
if not template:
|
|
63
63
|
return template
|
|
64
64
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if
|
|
65
|
+
def replace_template_match(match: re.Match) -> str:
|
|
66
|
+
template_path = match.group(1)
|
|
67
|
+
resolved_value = self._get_value(template_path)
|
|
68
|
+
if resolved_value is None:
|
|
69
69
|
# Keep the marker if value not found
|
|
70
70
|
return match.group(0)
|
|
71
|
-
return str(
|
|
71
|
+
return str(resolved_value)
|
|
72
72
|
|
|
73
|
-
return self.TEMPLATE_PATTERN.sub(
|
|
73
|
+
return self.TEMPLATE_PATTERN.sub(replace_template_match, template)
|
|
74
74
|
|
|
75
75
|
def _get_value(self, path: str) -> Any:
|
|
76
76
|
"""
|
|
@@ -82,29 +82,29 @@ class TemplateResolver:
|
|
|
82
82
|
Returns:
|
|
83
83
|
Value at path, or None if not found
|
|
84
84
|
"""
|
|
85
|
-
|
|
86
|
-
if not
|
|
85
|
+
path_segments = path.split(".")
|
|
86
|
+
if not path_segments:
|
|
87
87
|
return None
|
|
88
88
|
|
|
89
89
|
# First part is the namespace
|
|
90
|
-
|
|
91
|
-
namespace = self.namespaces.get(
|
|
90
|
+
namespace_key = path_segments[0]
|
|
91
|
+
namespace = self.namespaces.get(namespace_key)
|
|
92
92
|
if namespace is None:
|
|
93
93
|
return None
|
|
94
94
|
|
|
95
95
|
# Navigate nested keys
|
|
96
|
-
|
|
97
|
-
for part in
|
|
98
|
-
if isinstance(
|
|
99
|
-
|
|
96
|
+
current_value = namespace
|
|
97
|
+
for part in path_segments[1:]:
|
|
98
|
+
if isinstance(current_value, dict):
|
|
99
|
+
current_value = current_value.get(part)
|
|
100
100
|
else:
|
|
101
101
|
# Can't navigate further
|
|
102
102
|
return None
|
|
103
103
|
|
|
104
|
-
if
|
|
104
|
+
if current_value is None:
|
|
105
105
|
return None
|
|
106
106
|
|
|
107
|
-
return
|
|
107
|
+
return current_value
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def resolve_template(
|
tactus/core/yaml_parser.py
CHANGED
|
@@ -4,9 +4,9 @@ YAML Parser and Validator for Lua DSL Procedures.
|
|
|
4
4
|
Parses procedure YAML configurations and validates required structure.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import yaml
|
|
8
7
|
import logging
|
|
9
|
-
from typing import
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
import yaml
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ class ProcedureYAMLParser:
|
|
|
21
21
|
"""Parses and validates Lua DSL procedure YAML configurations."""
|
|
22
22
|
|
|
23
23
|
@staticmethod
|
|
24
|
-
def parse(yaml_content: str) ->
|
|
24
|
+
def parse(yaml_content: str) -> dict[str, Any]:
|
|
25
25
|
"""
|
|
26
26
|
Parse YAML content into a validated procedure configuration.
|
|
27
27
|
|
|
@@ -35,50 +35,53 @@ class ProcedureYAMLParser:
|
|
|
35
35
|
ProcedureConfigError: If YAML is invalid or missing required fields
|
|
36
36
|
"""
|
|
37
37
|
try:
|
|
38
|
-
|
|
39
|
-
except yaml.YAMLError as
|
|
40
|
-
raise ProcedureConfigError(f"Invalid YAML syntax: {
|
|
38
|
+
parsed_configuration = yaml.safe_load(yaml_content)
|
|
39
|
+
except yaml.YAMLError as exception:
|
|
40
|
+
raise ProcedureConfigError(f"Invalid YAML syntax: {exception}")
|
|
41
41
|
|
|
42
|
-
if not isinstance(
|
|
42
|
+
if not isinstance(parsed_configuration, dict):
|
|
43
43
|
raise ProcedureConfigError("YAML root must be a dictionary")
|
|
44
44
|
|
|
45
45
|
# Validate required top-level fields
|
|
46
|
-
ProcedureYAMLParser._validate_required_fields(
|
|
46
|
+
ProcedureYAMLParser._validate_required_fields(parsed_configuration)
|
|
47
47
|
|
|
48
48
|
# Validate specific sections
|
|
49
|
-
ProcedureYAMLParser._validate_params(
|
|
50
|
-
ProcedureYAMLParser._validate_outputs(
|
|
51
|
-
ProcedureYAMLParser._validate_default_model(
|
|
52
|
-
ProcedureYAMLParser._validate_default_provider(
|
|
53
|
-
ProcedureYAMLParser._validate_agents(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
ProcedureYAMLParser._validate_params(parsed_configuration.get("params", {}))
|
|
50
|
+
ProcedureYAMLParser._validate_outputs(parsed_configuration.get("output", {}))
|
|
51
|
+
ProcedureYAMLParser._validate_default_model(parsed_configuration.get("default_model"))
|
|
52
|
+
ProcedureYAMLParser._validate_default_provider(parsed_configuration.get("default_provider"))
|
|
53
|
+
ProcedureYAMLParser._validate_agents(
|
|
54
|
+
parsed_configuration.get("agents", {}), parsed_configuration
|
|
55
|
+
)
|
|
56
|
+
ProcedureYAMLParser._validate_procedure(parsed_configuration.get("procedure"))
|
|
57
|
+
|
|
58
|
+
logger.info("Successfully parsed procedure: %s", parsed_configuration.get("name"))
|
|
59
|
+
return parsed_configuration
|
|
58
60
|
|
|
59
61
|
@staticmethod
|
|
60
|
-
def _validate_required_fields(config:
|
|
62
|
+
def _validate_required_fields(config: dict[str, Any]) -> None:
|
|
61
63
|
"""Validate that required top-level fields are present."""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
required_fields = ["name", "version", "procedure"]
|
|
65
|
+
missing_fields = [field for field in required_fields if field not in config]
|
|
64
66
|
|
|
65
|
-
if
|
|
66
|
-
raise ProcedureConfigError(f"Missing required fields: {', '.join(
|
|
67
|
+
if missing_fields:
|
|
68
|
+
raise ProcedureConfigError(f"Missing required fields: {', '.join(missing_fields)}")
|
|
67
69
|
|
|
68
70
|
# Validate class field if present (for routing)
|
|
69
71
|
if "class" in config:
|
|
70
72
|
if config["class"] != "LuaDSL":
|
|
71
73
|
logger.warning(
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
+
"Procedure class '%s' may not be compatible with Lua DSL runtime",
|
|
75
|
+
config["class"],
|
|
74
76
|
)
|
|
75
77
|
|
|
76
78
|
@staticmethod
|
|
77
|
-
def _validate_params(params:
|
|
79
|
+
def _validate_params(params: dict[str, Any]) -> None:
|
|
78
80
|
"""Validate parameter definitions."""
|
|
79
81
|
if not isinstance(params, dict):
|
|
80
82
|
raise ProcedureConfigError("'params' must be a dictionary")
|
|
81
83
|
|
|
84
|
+
valid_types = ["string", "number", "boolean", "array", "object"]
|
|
82
85
|
for param_name, param_def in params.items():
|
|
83
86
|
if not isinstance(param_def, dict):
|
|
84
87
|
raise ProcedureConfigError(
|
|
@@ -87,7 +90,6 @@ class ProcedureYAMLParser:
|
|
|
87
90
|
|
|
88
91
|
# Validate type field if present
|
|
89
92
|
if "type" in param_def:
|
|
90
|
-
valid_types = ["string", "number", "boolean", "array", "object"]
|
|
91
93
|
if param_def["type"] not in valid_types:
|
|
92
94
|
raise ProcedureConfigError(
|
|
93
95
|
f"Parameter '{param_name}' has invalid type: {param_def['type']}. "
|
|
@@ -95,11 +97,12 @@ class ProcedureYAMLParser:
|
|
|
95
97
|
)
|
|
96
98
|
|
|
97
99
|
@staticmethod
|
|
98
|
-
def _validate_outputs(outputs:
|
|
100
|
+
def _validate_outputs(outputs: dict[str, Any]) -> None:
|
|
99
101
|
"""Validate output definitions."""
|
|
100
102
|
if not isinstance(outputs, dict):
|
|
101
103
|
raise ProcedureConfigError("'output' must be a dictionary")
|
|
102
104
|
|
|
105
|
+
valid_types = ["string", "number", "boolean", "array", "object"]
|
|
103
106
|
for output_name, output_def in outputs.items():
|
|
104
107
|
if not isinstance(output_def, dict):
|
|
105
108
|
raise ProcedureConfigError(
|
|
@@ -108,7 +111,6 @@ class ProcedureYAMLParser:
|
|
|
108
111
|
|
|
109
112
|
# Validate type field if present
|
|
110
113
|
if "type" in output_def:
|
|
111
|
-
valid_types = ["string", "number", "boolean", "array", "object"]
|
|
112
114
|
if output_def["type"] not in valid_types:
|
|
113
115
|
raise ProcedureConfigError(
|
|
114
116
|
f"Output '{output_name}' has invalid type: {output_def['type']}. "
|
|
@@ -141,7 +143,7 @@ class ProcedureYAMLParser:
|
|
|
141
143
|
)
|
|
142
144
|
|
|
143
145
|
@staticmethod
|
|
144
|
-
def _validate_agents(agents:
|
|
146
|
+
def _validate_agents(agents: dict[str, Any], parsed_configuration: dict[str, Any]) -> None:
|
|
145
147
|
"""Validate agent definitions."""
|
|
146
148
|
if not isinstance(agents, dict):
|
|
147
149
|
raise ProcedureConfigError("'agents' must be a dictionary")
|
|
@@ -149,17 +151,18 @@ class ProcedureYAMLParser:
|
|
|
149
151
|
if not agents:
|
|
150
152
|
raise ProcedureConfigError("At least one agent must be defined")
|
|
151
153
|
|
|
154
|
+
valid_providers = ["openai", "bedrock"]
|
|
152
155
|
for agent_name, agent_def in agents.items():
|
|
153
156
|
if not isinstance(agent_def, dict):
|
|
154
157
|
raise ProcedureConfigError(f"Agent '{agent_name}' definition must be a dictionary")
|
|
155
158
|
|
|
156
159
|
# Validate required agent fields
|
|
157
160
|
required_agent_fields = ["system_prompt", "initial_message"]
|
|
158
|
-
|
|
161
|
+
missing_fields = [field for field in required_agent_fields if field not in agent_def]
|
|
159
162
|
|
|
160
|
-
if
|
|
163
|
+
if missing_fields:
|
|
161
164
|
raise ProcedureConfigError(
|
|
162
|
-
f"Agent '{agent_name}' missing required fields: {', '.join(
|
|
165
|
+
f"Agent '{agent_name}' missing required fields: {', '.join(missing_fields)}"
|
|
163
166
|
)
|
|
164
167
|
|
|
165
168
|
# Validate model field if present
|
|
@@ -215,33 +218,41 @@ class ProcedureYAMLParser:
|
|
|
215
218
|
|
|
216
219
|
# Validate specific field types
|
|
217
220
|
if "temperature" in model_value:
|
|
218
|
-
|
|
219
|
-
if
|
|
221
|
+
temperature = model_value["temperature"]
|
|
222
|
+
if (
|
|
223
|
+
not isinstance(temperature, (int, float))
|
|
224
|
+
or temperature < 0
|
|
225
|
+
or temperature > 2
|
|
226
|
+
):
|
|
220
227
|
raise ProcedureConfigError(
|
|
221
228
|
f"Agent '{agent_name}' temperature must be a number between 0 and 2"
|
|
222
229
|
)
|
|
223
230
|
|
|
224
231
|
if "top_p" in model_value:
|
|
225
|
-
|
|
226
|
-
if
|
|
232
|
+
top_probability = model_value["top_p"]
|
|
233
|
+
if (
|
|
234
|
+
not isinstance(top_probability, (int, float))
|
|
235
|
+
or top_probability < 0
|
|
236
|
+
or top_probability > 1
|
|
237
|
+
):
|
|
227
238
|
raise ProcedureConfigError(
|
|
228
239
|
f"Agent '{agent_name}' top_p must be a number between 0 and 1"
|
|
229
240
|
)
|
|
230
241
|
|
|
231
242
|
if "max_tokens" in model_value:
|
|
232
|
-
|
|
233
|
-
if not isinstance(
|
|
243
|
+
max_tokens = model_value["max_tokens"]
|
|
244
|
+
if not isinstance(max_tokens, int) or max_tokens < 1:
|
|
234
245
|
raise ProcedureConfigError(
|
|
235
246
|
f"Agent '{agent_name}' max_tokens must be a positive integer"
|
|
236
247
|
)
|
|
237
248
|
|
|
238
249
|
if "openai_reasoning_effort" in model_value:
|
|
239
|
-
|
|
250
|
+
reasoning_effort = model_value["openai_reasoning_effort"]
|
|
240
251
|
valid_efforts = ["low", "medium", "high"]
|
|
241
|
-
if
|
|
252
|
+
if reasoning_effort not in valid_efforts:
|
|
242
253
|
raise ProcedureConfigError(
|
|
243
254
|
f"Agent '{agent_name}' openai_reasoning_effort must be one of: {', '.join(valid_efforts)}. "
|
|
244
|
-
f"Got: {
|
|
255
|
+
f"Got: {reasoning_effort}"
|
|
245
256
|
)
|
|
246
257
|
else:
|
|
247
258
|
raise ProcedureConfigError(
|
|
@@ -249,7 +260,7 @@ class ProcedureYAMLParser:
|
|
|
249
260
|
)
|
|
250
261
|
|
|
251
262
|
# Validate provider field - required unless default_provider is set
|
|
252
|
-
has_default_provider = "default_provider" in
|
|
263
|
+
has_default_provider = "default_provider" in parsed_configuration
|
|
253
264
|
if "provider" not in agent_def and not has_default_provider:
|
|
254
265
|
raise ProcedureConfigError(
|
|
255
266
|
f"Agent '{agent_name}' must specify 'provider' (or set 'default_provider' at procedure level)"
|
|
@@ -261,7 +272,6 @@ class ProcedureYAMLParser:
|
|
|
261
272
|
if not agent_def["provider"].strip():
|
|
262
273
|
raise ProcedureConfigError(f"Agent '{agent_name}' provider cannot be empty")
|
|
263
274
|
# Validate it's a known provider
|
|
264
|
-
valid_providers = ["openai", "bedrock"]
|
|
265
275
|
if agent_def["provider"] not in valid_providers:
|
|
266
276
|
raise ProcedureConfigError(
|
|
267
277
|
f"Agent '{agent_name}' provider must be one of: {', '.join(valid_providers)}. "
|
|
@@ -291,11 +301,11 @@ class ProcedureYAMLParser:
|
|
|
291
301
|
logger.warning("Procedure has unmatched parentheses - may have syntax errors")
|
|
292
302
|
|
|
293
303
|
@staticmethod
|
|
294
|
-
def extract_agent_names(config:
|
|
304
|
+
def extract_agent_names(config: dict[str, Any]) -> list[str]:
|
|
295
305
|
"""Extract list of agent names from configuration."""
|
|
296
306
|
return list(config.get("agents", {}).keys())
|
|
297
307
|
|
|
298
308
|
@staticmethod
|
|
299
|
-
def get_agent_config(config:
|
|
309
|
+
def get_agent_config(config: dict[str, Any], agent_name: str) -> Optional[dict[str, Any]]:
|
|
300
310
|
"""Get configuration for a specific agent."""
|
|
301
311
|
return config.get("agents", {}).get(agent_name)
|
tactus/docs/extractor.py
CHANGED
|
@@ -146,6 +146,9 @@ class TacFileExtractor:
|
|
|
146
146
|
for line in lines:
|
|
147
147
|
line = line.strip()
|
|
148
148
|
|
|
149
|
+
if not line:
|
|
150
|
+
continue
|
|
151
|
+
|
|
149
152
|
if line.startswith("Feature:"):
|
|
150
153
|
feature_name = line[8:].strip()
|
|
151
154
|
|
|
@@ -163,17 +166,15 @@ class TacFileExtractor:
|
|
|
163
166
|
current_scenario = line[9:].strip()
|
|
164
167
|
current_steps = []
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
# Extract keyword and text
|
|
169
|
+
else:
|
|
168
170
|
for keyword in ["Given", "When", "Then", "And", "But"]:
|
|
169
171
|
if line.startswith(keyword + " "):
|
|
170
172
|
step_text = line[len(keyword) + 1 :].strip()
|
|
171
173
|
current_steps.append(BDDStep(keyword=keyword, text=step_text))
|
|
172
174
|
break
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
feature_desc_lines.append(line)
|
|
175
|
+
else:
|
|
176
|
+
if not feature_name:
|
|
177
|
+
feature_desc_lines.append(line)
|
|
177
178
|
|
|
178
179
|
# Save last scenario
|
|
179
180
|
if current_scenario:
|