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.
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.2.dist-info/METADATA +1809 -0
  157. tactus-0.31.2.dist-info/RECORD +160 -0
  158. tactus-0.31.2.dist-info/WHEEL +4 -0
  159. tactus-0.31.2.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,49 @@
1
+ """
2
+ Tactus primitives - Lua-callable Python objects.
3
+
4
+ These primitives are injected into the Lua sandbox and provide
5
+ the core functionality for workflow execution.
6
+ """
7
+
8
+ from tactus.primitives.state import StatePrimitive
9
+ from tactus.primitives.control import IterationsPrimitive, StopPrimitive
10
+ from tactus.primitives.tool import ToolPrimitive
11
+ from tactus.primitives.log import LogPrimitive
12
+ from tactus.primitives.step import StepPrimitive, CheckpointPrimitive
13
+ from tactus.primitives.json import JsonPrimitive
14
+ from tactus.primitives.retry import RetryPrimitive
15
+ from tactus.primitives.file import FilePrimitive
16
+
17
+ from tactus.primitives.human import HumanPrimitive
18
+ from tactus.primitives.system import SystemPrimitive
19
+ from tactus.primitives.host import HostPrimitive
20
+
21
+ # MessageHistory primitive is now available
22
+ from tactus.primitives.message_history import MessageHistoryPrimitive
23
+
24
+ # NOTE: AgentPrimitive and ResultPrimitive have been replaced by DSPy implementation
25
+ # Agent functionality is now provided by tactus.dspy.agent
26
+
27
+ # These will be imported when their dependencies are ready
28
+ # from tactus.primitives.system import SystemPrimitive
29
+ # from tactus.primitives.procedure import ProcedurePrimitive
30
+ # from tactus.primitives.graph import GraphNodePrimitive
31
+
32
+ __all__ = [
33
+ "StatePrimitive",
34
+ "IterationsPrimitive",
35
+ "StopPrimitive",
36
+ "ToolPrimitive",
37
+ "HumanPrimitive",
38
+ "LogPrimitive",
39
+ "StepPrimitive",
40
+ "CheckpointPrimitive",
41
+ "MessageHistoryPrimitive",
42
+ "JsonPrimitive",
43
+ "RetryPrimitive",
44
+ "FilePrimitive",
45
+ "SystemPrimitive",
46
+ "HostPrimitive",
47
+ # "AgentPrimitive", # Replaced by DSPy implementation
48
+ # "ResultPrimitive", # Replaced by DSPy implementation
49
+ ]
@@ -0,0 +1,168 @@
1
+ """
2
+ Control Primitives - Flow control and termination.
3
+
4
+ Provides:
5
+ - Iterations.current() - Get current iteration number
6
+ - Iterations.exceeded(max) - Check if exceeded max iterations
7
+ - Stop.requested() - Check if stop was requested
8
+ - Stop.reason() - Get stop reason
9
+ - Stop.success() - Check if stop was successful
10
+ """
11
+
12
+ import logging
13
+ from typing import Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class IterationsPrimitive:
19
+ """
20
+ Tracks iteration count for procedure execution.
21
+
22
+ Provides safety limits and iteration-based control flow.
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize iteration counter."""
27
+ self._current_iteration = 0
28
+ logger.debug("IterationsPrimitive initialized")
29
+
30
+ def current(self) -> int:
31
+ """
32
+ Get the current iteration number.
33
+
34
+ Returns:
35
+ Current iteration count (0-indexed)
36
+
37
+ Example (Lua):
38
+ local iter = Iterations.current()
39
+ Log.info("Iteration: " .. iter)
40
+ """
41
+ return self._current_iteration
42
+
43
+ def exceeded(self, max_iterations: int) -> bool:
44
+ """
45
+ Check if current iteration has exceeded the maximum.
46
+
47
+ Args:
48
+ max_iterations: Maximum allowed iterations
49
+
50
+ Returns:
51
+ True if current iteration >= max_iterations
52
+
53
+ Example (Lua):
54
+ if Iterations.exceeded(100) then
55
+ return {success = false, reason = "Max iterations exceeded"}
56
+ end
57
+ """
58
+ exceeded = self._current_iteration >= max_iterations
59
+ if exceeded:
60
+ logger.warning(f"Iterations exceeded: {self._current_iteration} >= {max_iterations}")
61
+ return exceeded
62
+
63
+ def increment(self) -> int:
64
+ """
65
+ Increment the iteration counter.
66
+
67
+ Returns:
68
+ New iteration count
69
+
70
+ Note: This is called internally by the runtime, not from Lua
71
+ """
72
+ self._current_iteration += 1
73
+ logger.debug(f"Iteration incremented to {self._current_iteration}")
74
+ return self._current_iteration
75
+
76
+ def reset(self) -> None:
77
+ """Reset iteration counter (mainly for testing)."""
78
+ self._current_iteration = 0
79
+ logger.debug("Iterations reset to 0")
80
+
81
+ def __repr__(self) -> str:
82
+ return f"IterationsPrimitive(current={self._current_iteration})"
83
+
84
+
85
+ class StopPrimitive:
86
+ """
87
+ Manages procedure termination state.
88
+
89
+ Tracks when a stop was requested and the reason/success status.
90
+ """
91
+
92
+ def __init__(self):
93
+ """Initialize stop state."""
94
+ self._requested = False
95
+ self._reason: Optional[str] = None
96
+ self._success = True
97
+ logger.debug("StopPrimitive initialized")
98
+
99
+ def requested(self) -> bool:
100
+ """
101
+ Check if a stop was requested.
102
+
103
+ Returns:
104
+ True if stop was requested
105
+
106
+ Example (Lua):
107
+ if Stop.requested() then
108
+ return {success = true, message = "Procedure stopped"}
109
+ end
110
+ """
111
+ return self._requested
112
+
113
+ def reason(self) -> Optional[str]:
114
+ """
115
+ Get the reason for stopping.
116
+
117
+ Returns:
118
+ Stop reason string or None if not stopped
119
+
120
+ Example (Lua):
121
+ if Stop.requested() then
122
+ Log.info("Stopped because: " .. Stop.reason())
123
+ end
124
+ """
125
+ return self._reason
126
+
127
+ def success(self) -> bool:
128
+ """
129
+ Check if the stop was due to successful completion.
130
+
131
+ Returns:
132
+ True if stopped successfully, False if stopped due to error
133
+
134
+ Example (Lua):
135
+ if Stop.requested() and Stop.success() then
136
+ return {success = true}
137
+ else
138
+ return {success = false}
139
+ end
140
+ """
141
+ return self._success
142
+
143
+ def request(self, reason: str, success: bool = True) -> None:
144
+ """
145
+ Request a stop (called by tools or runtime).
146
+
147
+ Args:
148
+ reason: Reason for stopping
149
+ success: Whether this is a successful completion
150
+
151
+ Note: This is called internally, not from Lua
152
+ """
153
+ self._requested = True
154
+ self._reason = reason
155
+ self._success = success
156
+
157
+ log_level = logging.INFO if success else logging.WARNING
158
+ logger.log(log_level, f"Stop requested: {reason} (success={success})")
159
+
160
+ def reset(self) -> None:
161
+ """Reset stop state (mainly for testing)."""
162
+ self._requested = False
163
+ self._reason = None
164
+ self._success = True
165
+ logger.debug("Stop state reset")
166
+
167
+ def __repr__(self) -> str:
168
+ return f"StopPrimitive(requested={self._requested}, success={self._success})"
@@ -0,0 +1,229 @@
1
+ """
2
+ File Primitive - File I/O operations for workflows.
3
+
4
+ Provides:
5
+ - File.read(path) - Read file contents
6
+ - File.write(path, content) - Write content to file
7
+ - File.exists(path) - Check if file exists
8
+ - File.size(path) - Get file size in bytes
9
+ """
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class FilePrimitive:
19
+ """
20
+ Handles file operations for procedures.
21
+
22
+ Enables workflows to:
23
+ - Read file contents
24
+ - Write data to files
25
+ - Check file existence
26
+ - Get file metadata
27
+
28
+ Note: File operations are non-deterministic (files can change between executions).
29
+ Wrap in Step.checkpoint() for durability.
30
+ """
31
+
32
+ def __init__(self, base_path: Optional[str] = None, execution_context=None):
33
+ """
34
+ Initialize File primitive.
35
+
36
+ Args:
37
+ base_path: Optional base directory for relative paths (defaults to cwd)
38
+ execution_context: Optional ExecutionContext for determinism checking
39
+ """
40
+ self.base_path = Path(base_path) if base_path else Path.cwd()
41
+ self.execution_context = execution_context
42
+ logger.debug(f"FilePrimitive initialized with base_path: {self.base_path}")
43
+
44
+ def _check_determinism(self, operation: str):
45
+ """Warn if file operation called outside checkpoint."""
46
+ if self.execution_context and not getattr(
47
+ self.execution_context, "_inside_checkpoint", False
48
+ ):
49
+ import warnings
50
+
51
+ warnings.warn(
52
+ f"\n{'=' * 70}\n"
53
+ f"DETERMINISM WARNING: File.{operation}() called outside checkpoint\n"
54
+ f"{'=' * 70}\n\n"
55
+ f"File operations are non-deterministic - "
56
+ f"file contents can change between executions.\n\n"
57
+ f"To fix, wrap in Step.checkpoint():\n\n"
58
+ f" state.data = Step.checkpoint(function()\n"
59
+ f" return File.{operation}(...)\n"
60
+ f" end)\n\n"
61
+ f"Why: Files can be modified, deleted, or created "
62
+ f"between procedure executions,\n"
63
+ f"causing different behavior on replay.\n"
64
+ f"\n{'=' * 70}\n",
65
+ UserWarning,
66
+ stacklevel=3,
67
+ )
68
+
69
+ def read(self, path: str) -> str:
70
+ """
71
+ Read file contents as string.
72
+
73
+ Args:
74
+ path: File path to read (absolute or relative to base_path)
75
+
76
+ Returns:
77
+ File contents as string
78
+
79
+ Raises:
80
+ FileNotFoundError: If file doesn't exist
81
+ IOError: If file cannot be read
82
+
83
+ Example (Lua):
84
+ local config = File.read("config.json")
85
+ Log.info("Config loaded", {length = #config})
86
+ """
87
+ self._check_determinism("read")
88
+ file_path = self._resolve_path(path)
89
+
90
+ try:
91
+ logger.debug(f"Reading file: {file_path}")
92
+ with open(file_path, "r", encoding="utf-8") as f:
93
+ content = f.read()
94
+ logger.info(f"Read {len(content)} bytes from {file_path}")
95
+ return content
96
+
97
+ except FileNotFoundError:
98
+ error_msg = f"File not found: {file_path}"
99
+ logger.error(error_msg)
100
+ raise FileNotFoundError(error_msg)
101
+
102
+ except Exception as e:
103
+ error_msg = f"Failed to read file {file_path}: {e}"
104
+ logger.error(error_msg)
105
+ raise IOError(error_msg)
106
+
107
+ def write(self, path: str, content: str) -> bool:
108
+ """
109
+ Write content to file.
110
+
111
+ Args:
112
+ path: File path to write (absolute or relative to base_path)
113
+ content: Content to write
114
+
115
+ Returns:
116
+ True if successful
117
+
118
+ Raises:
119
+ IOError: If file cannot be written
120
+
121
+ Example (Lua):
122
+ local data = Json.encode({status = "complete"})
123
+ File.write("output.json", data)
124
+ Log.info("Data written")
125
+ """
126
+ self._check_determinism("write")
127
+ file_path = self._resolve_path(path)
128
+
129
+ try:
130
+ # Create parent directories if needed
131
+ file_path.parent.mkdir(parents=True, exist_ok=True)
132
+
133
+ logger.debug(f"Writing to file: {file_path}")
134
+ with open(file_path, "w", encoding="utf-8") as f:
135
+ f.write(content)
136
+
137
+ logger.info(f"Wrote {len(content)} bytes to {file_path}")
138
+ return True
139
+
140
+ except Exception as e:
141
+ error_msg = f"Failed to write file {file_path}: {e}"
142
+ logger.error(error_msg)
143
+ raise IOError(error_msg)
144
+
145
+ def exists(self, path: str) -> bool:
146
+ """
147
+ Check if file exists.
148
+
149
+ Args:
150
+ path: File path to check (absolute or relative to base_path)
151
+
152
+ Returns:
153
+ True if file exists, False otherwise
154
+
155
+ Example (Lua):
156
+ if File.exists("cache.json") then
157
+ local data = File.read("cache.json")
158
+ Log.info("Using cached data")
159
+ else
160
+ Log.info("No cache found")
161
+ end
162
+ """
163
+ self._check_determinism("exists")
164
+ file_path = self._resolve_path(path)
165
+ exists = file_path.exists() and file_path.is_file()
166
+ logger.debug(f"File exists check for {file_path}: {exists}")
167
+ return exists
168
+
169
+ def size(self, path: str) -> int:
170
+ """
171
+ Get file size in bytes.
172
+
173
+ Args:
174
+ path: File path to check (absolute or relative to base_path)
175
+
176
+ Returns:
177
+ File size in bytes
178
+
179
+ Raises:
180
+ FileNotFoundError: If file doesn't exist
181
+
182
+ Example (Lua):
183
+ local size = File.size("data.csv")
184
+ Log.info("File size", {bytes = size, kb = size / 1024})
185
+ """
186
+ self._check_determinism("size")
187
+ file_path = self._resolve_path(path)
188
+
189
+ if not file_path.exists():
190
+ error_msg = f"File not found: {file_path}"
191
+ logger.error(error_msg)
192
+ raise FileNotFoundError(error_msg)
193
+
194
+ size = file_path.stat().st_size
195
+ logger.debug(f"File size for {file_path}: {size} bytes")
196
+ return size
197
+
198
+ def _resolve_path(self, path: str) -> Path:
199
+ """
200
+ Resolve file path relative to base_path with security validation.
201
+
202
+ Args:
203
+ path: File path to resolve (must be relative)
204
+
205
+ Returns:
206
+ Resolved Path object
207
+
208
+ Raises:
209
+ ValueError: If absolute path or path traversal detected
210
+ """
211
+ path_obj = Path(path)
212
+
213
+ # Security: Never allow absolute paths
214
+ if path_obj.is_absolute():
215
+ raise ValueError(f"Absolute paths not allowed: {path}")
216
+
217
+ # Resolve relative to base_path
218
+ resolved = (self.base_path / path_obj).resolve()
219
+
220
+ # Security: Verify resolved path is under base_path
221
+ try:
222
+ resolved.relative_to(self.base_path)
223
+ except ValueError:
224
+ raise ValueError(f"Path traversal detected: {path} resolves outside base directory")
225
+
226
+ return resolved
227
+
228
+ def __repr__(self) -> str:
229
+ return f"FilePrimitive(base_path={self.base_path})"