tactus 0.31.2__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 +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.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
|
+
]
|