tactus 0.31.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 (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.0.dist-info/METADATA +1809 -0
  157. tactus-0.31.0.dist-info/RECORD +160 -0
  158. tactus-0.31.0.dist-info/WHEEL +4 -0
  159. tactus-0.31.0.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,291 @@
1
+ """
2
+ Output Schema Validator for Lua DSL Procedures
3
+
4
+ Validates that Lua workflow return values match the declared output schema.
5
+ Enables type safety and composability for sub-agent workflows.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Optional, List
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class OutputValidationError(Exception):
15
+ """Raised when workflow output doesn't match schema."""
16
+
17
+ pass
18
+
19
+
20
+ class OutputValidator:
21
+ """
22
+ Validates procedure output against declared schema.
23
+
24
+ Supports:
25
+ - Type checking (string, number, boolean, object, array)
26
+ - Required field validation
27
+ - Nested object validation
28
+ - Clear error messages
29
+ """
30
+
31
+ # Type mapping from YAML to Python
32
+ TYPE_MAP = {
33
+ "string": str,
34
+ "number": (int, float),
35
+ "boolean": bool,
36
+ "object": dict,
37
+ "array": list,
38
+ }
39
+
40
+ @classmethod
41
+ def _is_scalar_schema(cls, schema: Any) -> bool:
42
+ return (
43
+ isinstance(schema, dict)
44
+ and "type" in schema
45
+ and isinstance(schema.get("type"), str)
46
+ and schema.get("type") in cls.TYPE_MAP
47
+ )
48
+
49
+ def __init__(self, output_schema: Optional[Any] = None):
50
+ """
51
+ Initialize validator with output schema.
52
+
53
+ Args:
54
+ output_schema: Dict of output field definitions
55
+ Example:
56
+ {
57
+ 'limerick': {
58
+ 'type': 'string',
59
+ 'required': True,
60
+ 'description': 'The generated limerick'
61
+ },
62
+ 'node_id': {
63
+ 'type': 'string',
64
+ 'required': False
65
+ }
66
+ }
67
+ """
68
+ self.schema = output_schema or {}
69
+
70
+ if self._is_scalar_schema(self.schema):
71
+ logger.debug("OutputValidator initialized with scalar output schema")
72
+ else:
73
+ try:
74
+ field_count = len(self.schema)
75
+ except TypeError:
76
+ field_count = 0
77
+ logger.debug(f"OutputValidator initialized with {field_count} output fields")
78
+
79
+ def validate(self, output: Any) -> Any:
80
+ """
81
+ Validate workflow output against schema.
82
+
83
+ Args:
84
+ output: The return value from Lua workflow
85
+
86
+ Returns:
87
+ Validated output dict
88
+
89
+ Raises:
90
+ OutputValidationError: If validation fails
91
+ """
92
+ # If a procedure returns a Result wrapper, validate its `.output` payload
93
+ # while preserving the wrapper (so callers can still access usage/cost/etc.).
94
+ from tactus.protocols.result import TactusResult
95
+
96
+ wrapped_result: TactusResult | None = output if isinstance(output, TactusResult) else None
97
+ if wrapped_result is not None:
98
+ output = wrapped_result.output
99
+
100
+ # If no schema defined, accept any output
101
+ if not self.schema:
102
+ logger.debug("No output schema defined, skipping validation")
103
+ if isinstance(output, dict):
104
+ validated_payload = output
105
+ elif hasattr(output, "items"):
106
+ # Lua table - convert to dict
107
+ validated_payload = dict(output.items())
108
+ else:
109
+ validated_payload = output
110
+
111
+ if wrapped_result is not None:
112
+ return wrapped_result.model_copy(update={"output": validated_payload})
113
+ return validated_payload
114
+
115
+ # Scalar output schema: `output = field.string{...}` etc.
116
+ if self._is_scalar_schema(self.schema):
117
+ # Lua tables are not valid scalar outputs.
118
+ if hasattr(output, "items") and not isinstance(output, dict):
119
+ output = dict(output.items())
120
+
121
+ is_required = self.schema.get("required", False)
122
+ if output is None and not is_required:
123
+ return None
124
+
125
+ expected_type = self.schema.get("type")
126
+ if expected_type and not self._check_type(output, expected_type):
127
+ raise OutputValidationError(
128
+ f"Output should be {expected_type}, got {type(output).__name__}"
129
+ )
130
+
131
+ if "enum" in self.schema and self.schema["enum"]:
132
+ allowed_values = self.schema["enum"]
133
+ if output not in allowed_values:
134
+ raise OutputValidationError(
135
+ f"Output has invalid value '{output}'. Allowed values: {allowed_values}"
136
+ )
137
+
138
+ validated_payload = output
139
+ if wrapped_result is not None:
140
+ return wrapped_result.model_copy(update={"output": validated_payload})
141
+ return validated_payload
142
+
143
+ # Convert Lua tables to dicts recursively
144
+ if hasattr(output, "items") or isinstance(output, dict):
145
+ logger.debug("Converting Lua tables to Python dicts recursively")
146
+ output = self._convert_lua_tables(output)
147
+
148
+ # Output must be a dict/table
149
+ if not isinstance(output, dict):
150
+ raise OutputValidationError(
151
+ f"Output must be an object/table, got {type(output).__name__}"
152
+ )
153
+
154
+ errors = []
155
+ validated_output = {}
156
+
157
+ # Check required fields and validate types
158
+ for field_name, field_def in self.schema.items():
159
+ if not isinstance(field_def, dict) or "type" not in field_def:
160
+ errors.append(
161
+ f"Field '{field_name}' uses old type syntax. "
162
+ f"Use field.{field_def.get('type', 'string')}{{}} instead."
163
+ )
164
+ continue
165
+ is_required = bool(field_def.get("required", False))
166
+
167
+ if is_required and field_name not in output:
168
+ errors.append(f"Required field '{field_name}' is missing")
169
+ continue
170
+
171
+ # Skip validation if field not present and not required
172
+ if field_name not in output:
173
+ continue
174
+
175
+ value = output[field_name]
176
+
177
+ # Type checking
178
+ expected_type = field_def.get("type")
179
+ if expected_type:
180
+ if not self._check_type(value, expected_type):
181
+ actual_type = type(value).__name__
182
+ errors.append(
183
+ f"Field '{field_name}' should be {expected_type}, got {actual_type}"
184
+ )
185
+
186
+ # Enum validation
187
+ if "enum" in field_def and field_def["enum"]:
188
+ allowed_values = field_def["enum"]
189
+ if value not in allowed_values:
190
+ errors.append(
191
+ f"Field '{field_name}' has invalid value '{value}'. "
192
+ f"Allowed values: {allowed_values}"
193
+ )
194
+
195
+ # Add to validated output (only declared fields)
196
+ validated_output[field_name] = value
197
+
198
+ # Filter undeclared fields (only return declared fields)
199
+ for field_name in output:
200
+ if field_name not in self.schema:
201
+ logger.debug(f"Filtering undeclared field '{field_name}' from output")
202
+
203
+ if errors:
204
+ error_msg = "Output validation failed:\n " + "\n ".join(errors)
205
+ raise OutputValidationError(error_msg)
206
+
207
+ logger.info(f"Output validation passed for {len(validated_output)} fields")
208
+ if wrapped_result is not None:
209
+ return wrapped_result.model_copy(update={"output": validated_output})
210
+ return validated_output
211
+
212
+ def _check_type(self, value: Any, expected_type: str) -> bool:
213
+ """
214
+ Check if value matches expected type.
215
+
216
+ Args:
217
+ value: The value to check
218
+ expected_type: Expected type string ('string', 'number', etc.)
219
+
220
+ Returns:
221
+ True if type matches
222
+ """
223
+ if value is None:
224
+ # None is acceptable for optional fields
225
+ return True
226
+
227
+ python_type = self.TYPE_MAP.get(expected_type)
228
+ if not python_type:
229
+ logger.warning(f"Unknown type '{expected_type}', skipping validation")
230
+ return True
231
+
232
+ # Handle Lua tables as dicts/arrays
233
+ if expected_type in ("object", "array"):
234
+ if hasattr(value, "items") or hasattr(value, "__iter__"):
235
+ return True
236
+
237
+ return isinstance(value, python_type)
238
+
239
+ def _convert_lua_tables(self, obj: Any) -> Any:
240
+ """
241
+ Recursively convert Lua tables to Python dicts/lists.
242
+
243
+ Args:
244
+ obj: Object to convert
245
+
246
+ Returns:
247
+ Converted object
248
+ """
249
+ # Handle Lua tables (have .items() method)
250
+ if hasattr(obj, "items") and not isinstance(obj, dict):
251
+ return {k: self._convert_lua_tables(v) for k, v in obj.items()}
252
+
253
+ # Handle lists
254
+ elif isinstance(obj, (list, tuple)):
255
+ return [self._convert_lua_tables(item) for item in obj]
256
+
257
+ # Handle dicts
258
+ elif isinstance(obj, dict):
259
+ return {k: self._convert_lua_tables(v) for k, v in obj.items()}
260
+
261
+ # Return as-is for primitives
262
+ else:
263
+ return obj
264
+
265
+ def get_field_description(self, field_name: str) -> Optional[str]:
266
+ """Get description for an output field."""
267
+ if field_name in self.schema:
268
+ field_def = self.schema[field_name]
269
+ if isinstance(field_def, dict):
270
+ return field_def.get("description")
271
+ return None
272
+
273
+ def get_required_fields(self) -> List[str]:
274
+ """Get list of required output fields."""
275
+ from tactus.core.dsl_stubs import FieldDefinition
276
+
277
+ return [
278
+ name
279
+ for name, def_ in self.schema.items()
280
+ if isinstance(def_, FieldDefinition) and def_.get("required", False)
281
+ ]
282
+
283
+ def get_optional_fields(self) -> List[str]:
284
+ """Get list of optional output fields."""
285
+ from tactus.core.dsl_stubs import FieldDefinition
286
+
287
+ return [
288
+ name
289
+ for name, def_ in self.schema.items()
290
+ if isinstance(def_, FieldDefinition) and not def_.get("required", False)
291
+ ]