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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -62,15 +62,15 @@ class TemplateResolver:
62
62
  if not template:
63
63
  return template
64
64
 
65
- def replace_match(match):
66
- path = match.group(1)
67
- value = self._get_value(path)
68
- if value is None:
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(value)
71
+ return str(resolved_value)
72
72
 
73
- return self.TEMPLATE_PATTERN.sub(replace_match, template)
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
- parts = path.split(".")
86
- if not parts:
85
+ path_segments = path.split(".")
86
+ if not path_segments:
87
87
  return None
88
88
 
89
89
  # First part is the namespace
90
- namespace_name = parts[0]
91
- namespace = self.namespaces.get(namespace_name)
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
- current = namespace
97
- for part in parts[1:]:
98
- if isinstance(current, dict):
99
- current = current.get(part)
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 current is None:
104
+ if current_value is None:
105
105
  return None
106
106
 
107
- return current
107
+ return current_value
108
108
 
109
109
 
110
110
  def resolve_template(
@@ -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 Dict, Any, Optional, List
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) -> Dict[str, Any]:
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
- config = yaml.safe_load(yaml_content)
39
- except yaml.YAMLError as e:
40
- raise ProcedureConfigError(f"Invalid YAML syntax: {e}")
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(config, dict):
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(config)
46
+ ProcedureYAMLParser._validate_required_fields(parsed_configuration)
47
47
 
48
48
  # Validate specific sections
49
- ProcedureYAMLParser._validate_params(config.get("params", {}))
50
- ProcedureYAMLParser._validate_outputs(config.get("output", {}))
51
- ProcedureYAMLParser._validate_default_model(config.get("default_model"))
52
- ProcedureYAMLParser._validate_default_provider(config.get("default_provider"))
53
- ProcedureYAMLParser._validate_agents(config.get("agents", {}), config)
54
- ProcedureYAMLParser._validate_procedure(config.get("procedure"))
55
-
56
- logger.info(f"Successfully parsed procedure: {config.get('name')}")
57
- return config
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: Dict[str, Any]) -> None:
62
+ def _validate_required_fields(config: dict[str, Any]) -> None:
61
63
  """Validate that required top-level fields are present."""
62
- required = ["name", "version", "procedure"]
63
- missing = [field for field in required if field not in config]
64
+ required_fields = ["name", "version", "procedure"]
65
+ missing_fields = [field for field in required_fields if field not in config]
64
66
 
65
- if missing:
66
- raise ProcedureConfigError(f"Missing required fields: {', '.join(missing)}")
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
- f"Procedure class '{config['class']}' may not be compatible "
73
- "with Lua DSL runtime"
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: Dict[str, Any]) -> None:
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: Dict[str, Any]) -> None:
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: Dict[str, Any], config: Dict[str, Any]) -> None:
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
- missing = [field for field in required_agent_fields if field not in agent_def]
161
+ missing_fields = [field for field in required_agent_fields if field not in agent_def]
159
162
 
160
- if missing:
163
+ if missing_fields:
161
164
  raise ProcedureConfigError(
162
- f"Agent '{agent_name}' missing required fields: {', '.join(missing)}"
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
- temp = model_value["temperature"]
219
- if not isinstance(temp, (int, float)) or temp < 0 or temp > 2:
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
- top_p = model_value["top_p"]
226
- if not isinstance(top_p, (int, float)) or top_p < 0 or top_p > 1:
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
- max_tok = model_value["max_tokens"]
233
- if not isinstance(max_tok, int) or max_tok < 1:
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
- effort = model_value["openai_reasoning_effort"]
250
+ reasoning_effort = model_value["openai_reasoning_effort"]
240
251
  valid_efforts = ["low", "medium", "high"]
241
- if effort not in valid_efforts:
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: {effort}"
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 config
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: Dict[str, Any]) -> List[str]:
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: Dict[str, Any], agent_name: str) -> Optional[Dict[str, Any]]:
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
- elif any(line.startswith(kw) for kw in ["Given ", "When ", "Then ", "And ", "But "]):
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
- elif line and not feature_name:
175
- # Description line before first scenario
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: