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
tactus/dspy/module.py ADDED
@@ -0,0 +1,405 @@
1
+ """
2
+ DSPy Module integration for Tactus.
3
+
4
+ This module provides the Module primitive that maps to DSPy modules,
5
+ supporting various prediction strategies like Predict, ChainOfThought, etc.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, Optional, Union
10
+
11
+ import dspy
12
+
13
+ from tactus.dspy.signature import create_signature
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class RawModule(dspy.Module):
19
+ """
20
+ Minimal DSPy module for raw LM calls without formatting delimiters.
21
+
22
+ This module calls the LM directly without using dspy.Predict, eliminating
23
+ the formatting delimiters like [[ ## response ## ]] that Predict adds.
24
+
25
+ Key features:
26
+ - No DSPy formatting delimiters in output
27
+ - Works correctly with dspy.streamify() for streaming
28
+ - Maintains DSPy's optimization and tracing capabilities
29
+ - Supports dynamic signatures (with or without tool_calls)
30
+
31
+ Usage:
32
+ # Without tools
33
+ raw = RawModule(signature="system_prompt, history, user_message -> response")
34
+ result = raw(user_message="Hello", history="")
35
+
36
+ # With tools
37
+ raw = RawModule(signature="system_prompt, history, user_message, available_tools -> response, tool_calls")
38
+ result = raw(user_message="Hello", history="", available_tools="...")
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ signature: str = "system_prompt, history, user_message -> response",
44
+ system_prompt: str = "",
45
+ ):
46
+ """
47
+ Initialize raw module.
48
+
49
+ Args:
50
+ signature: DSPy signature string defining inputs and outputs
51
+ system_prompt: System prompt to prepend to all conversations
52
+ """
53
+ super().__init__()
54
+ self.system_prompt = system_prompt
55
+ self.signature = signature
56
+ # Parse signature to determine output fields
57
+ self.output_fields = self._parse_output_fields(signature)
58
+
59
+ def _parse_output_fields(self, signature: str) -> list:
60
+ """Extract output field names from signature string."""
61
+ if "->" not in signature:
62
+ return ["response"]
63
+ output_part = signature.split("->")[1].strip()
64
+ return [field.strip() for field in output_part.split(",")]
65
+
66
+ def forward(
67
+ self, system_prompt: str, history, user_message: str, available_tools: str = "", **kwargs
68
+ ):
69
+ """
70
+ Forward pass with direct LM call (no formatting delimiters).
71
+
72
+ Args:
73
+ system_prompt: System prompt (overrides init if provided)
74
+ history: Conversation history (dspy.History, TactusHistory, or string)
75
+ user_message: Current user message
76
+ available_tools: Optional tools description (for agents with tools)
77
+ **kwargs: Additional args passed to LM
78
+
79
+ Returns:
80
+ dspy.Prediction with response field (and tool_calls if signature includes it)
81
+ """
82
+ # Use provided system_prompt or fall back to init value
83
+ sys_prompt = system_prompt or self.system_prompt
84
+
85
+ # Build messages array for direct LM call
86
+ messages = []
87
+
88
+ # Add system prompt if provided
89
+ if sys_prompt:
90
+ messages.append({"role": "system", "content": sys_prompt})
91
+
92
+ # Add history messages
93
+ if history:
94
+ if hasattr(history, "messages"):
95
+ # It's a History object - use messages directly
96
+ messages.extend(history.messages)
97
+ elif isinstance(history, str) and history.strip():
98
+ # It's a formatted string - parse it
99
+ for line in history.strip().split("\n"):
100
+ if line.startswith("User: "):
101
+ messages.append({"role": "user", "content": line[6:]})
102
+ elif line.startswith("Assistant: "):
103
+ messages.append({"role": "assistant", "content": line[11:]})
104
+
105
+ # Add current user message
106
+ if user_message:
107
+ # If tools are available, include them in the user message
108
+ if available_tools and "available_tools" in self.signature:
109
+ user_content = f"{user_message}\n\nAvailable tools:\n{available_tools}"
110
+ messages.append({"role": "user", "content": user_content})
111
+ else:
112
+ messages.append({"role": "user", "content": user_message})
113
+
114
+ # Get the configured LM
115
+ lm = dspy.settings.lm
116
+ if lm is None:
117
+ raise RuntimeError("No LM configured. Call dspy.configure(lm=...) first.")
118
+
119
+ # Call LM directly - streamify() will intercept this call if streaming is enabled
120
+ response = lm(messages=messages, **kwargs)
121
+
122
+ # Extract response text from LM result
123
+ # LM returns a list of strings - take the first one
124
+ response_text = response[0] if isinstance(response, list) else str(response)
125
+
126
+ # Build prediction result based on signature
127
+ prediction_kwargs = {"response": response_text}
128
+
129
+ # If signature includes tool_calls, add a placeholder
130
+ # (Real tool call parsing would happen here in a full implementation)
131
+ if "tool_calls" in self.output_fields:
132
+ prediction_kwargs["tool_calls"] = "No tools were used."
133
+
134
+ # Return as Prediction for DSPy compatibility
135
+ return dspy.Prediction(**prediction_kwargs)
136
+
137
+
138
+ class TactusModule:
139
+ """
140
+ A Tactus wrapper around DSPy modules.
141
+
142
+ This class creates callable DSPy modules based on the specified strategy.
143
+ It handles both string and structured signatures and supports different
144
+ DSPy module strategies.
145
+
146
+ Supported strategies:
147
+ - "predict": Uses dspy.Predict for direct prediction
148
+ - "chain_of_thought": Uses dspy.ChainOfThought for reasoning
149
+ - "react": Uses dspy.ReAct for reasoning + action (coming in Step 5.1)
150
+ - "program_of_thought": Uses dspy.ProgramOfThought (coming in Step 5.2)
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ name: str,
156
+ signature: Union[str, Dict[str, Any], dspy.Signature],
157
+ strategy: str = "predict",
158
+ input_schema: Optional[Dict[str, Any]] = None,
159
+ output_schema: Optional[Dict[str, Any]] = None,
160
+ **kwargs: Any,
161
+ ):
162
+ """
163
+ Initialize a Tactus Module.
164
+
165
+ Args:
166
+ name: Name for this module (used for tracking/optimization)
167
+ signature: Either a string ("question -> answer"), a dict with
168
+ input/output definitions, or a DSPy Signature
169
+ strategy: The DSPy module strategy to use ("predict", "chain_of_thought")
170
+ input_schema: Optional explicit input schema (derived from signature if not provided)
171
+ output_schema: Optional explicit output schema (derived from signature if not provided)
172
+ **kwargs: Additional configuration passed to the DSPy module
173
+ """
174
+ self.name = name
175
+ self.strategy = strategy
176
+ self.kwargs = kwargs
177
+
178
+ # Resolve signature
179
+ if isinstance(signature, str) or isinstance(signature, dict):
180
+ self.signature = create_signature(signature)
181
+ else:
182
+ self.signature = signature
183
+
184
+ # Store explicit schemas or derive from signature
185
+ self.input_schema = input_schema or self._derive_input_schema()
186
+ self.output_schema = output_schema or self._derive_output_schema()
187
+
188
+ # Create the DSPy module based on strategy
189
+ self.module = self._create_module()
190
+
191
+ def _derive_input_schema(self) -> Dict[str, Any]:
192
+ """Derive input schema from the DSPy signature."""
193
+ schema = {}
194
+ if hasattr(self.signature, "input_fields"):
195
+ for field_name, field_info in self.signature.input_fields.items():
196
+ schema[field_name] = {
197
+ "type": "string",
198
+ "required": True,
199
+ "description": getattr(field_info, "description", None) or field_name,
200
+ }
201
+ return schema
202
+
203
+ def _derive_output_schema(self) -> Dict[str, Any]:
204
+ """Derive output schema from the DSPy signature."""
205
+ schema = {}
206
+ if hasattr(self.signature, "output_fields"):
207
+ for field_name, field_info in self.signature.output_fields.items():
208
+ schema[field_name] = {
209
+ "type": "string",
210
+ "required": True,
211
+ "description": getattr(field_info, "description", None) or field_name,
212
+ }
213
+ return schema
214
+
215
+ def _create_module(self) -> dspy.Module:
216
+ """Create the appropriate DSPy module based on strategy.
217
+
218
+ Passes through any extra kwargs to the DSPy module constructor,
219
+ allowing access to DSPy-specific options like temperature, max_tokens,
220
+ rationale_field (for ChainOfThought), etc.
221
+ """
222
+ if self.strategy == "predict":
223
+ return dspy.Predict(self.signature, **self.kwargs)
224
+ elif self.strategy == "chain_of_thought":
225
+ return dspy.ChainOfThought(self.signature, **self.kwargs)
226
+ elif self.strategy == "raw":
227
+ # Raw module for minimal formatting
228
+ # Pass the signature so RawModule can support tool_calls when needed
229
+ if isinstance(self.signature, str):
230
+ signature_str = self.signature
231
+ else:
232
+ # It's a DSPy Signature object - reconstruct the signature string
233
+ # from input_fields and output_fields
234
+ input_names = list(self.signature.input_fields.keys())
235
+ output_names = list(self.signature.output_fields.keys())
236
+ signature_str = f"{', '.join(input_names)} -> {', '.join(output_names)}"
237
+ return RawModule(signature=signature_str, system_prompt="")
238
+ elif self.strategy == "react":
239
+ # ReAct requires tools - will be implemented in Step 5.1
240
+ raise NotImplementedError("ReAct strategy not yet implemented. Coming in Step 5.1.")
241
+ elif self.strategy == "program_of_thought":
242
+ # ProgramOfThought - will be implemented in Step 5.2
243
+ raise NotImplementedError(
244
+ "ProgramOfThought strategy not yet implemented. Coming in Step 5.2."
245
+ )
246
+ else:
247
+ raise ValueError(
248
+ f"Unknown strategy '{self.strategy}'. Supported: predict, chain_of_thought, raw"
249
+ )
250
+
251
+ def __call__(self, **kwargs: Any) -> dspy.Prediction:
252
+ """
253
+ Execute the module with the given inputs.
254
+
255
+ Args:
256
+ **kwargs: Input values matching the signature's input fields
257
+
258
+ Returns:
259
+ A DSPy Prediction object with output fields accessible as attributes
260
+ """
261
+ return self.module(**kwargs)
262
+
263
+
264
+ def create_module(
265
+ name: str,
266
+ config: Dict[str, Any],
267
+ registry: Any = None,
268
+ mock_manager: Any = None,
269
+ ) -> "LuaCallableModule":
270
+ """
271
+ Create a Tactus Module from configuration.
272
+
273
+ This is the main entry point used by the DSL stubs.
274
+
275
+ Args:
276
+ name: Name for the module
277
+ config: Configuration dict with:
278
+ - signature: String or structured signature definition
279
+ - strategy: Module strategy (default: "predict")
280
+ - input: Optional explicit input schema
281
+ - output: Optional explicit output schema
282
+ - Other optional configuration
283
+ registry: Optional Registry instance for accessing mocks
284
+ mock_manager: Optional MockManager instance for checking mocks
285
+
286
+ Returns:
287
+ A Lua-callable wrapper around a TactusModule instance
288
+ """
289
+ signature = config.get("signature")
290
+ if signature is None:
291
+ raise ValueError(
292
+ f"Module '{name}' requires a 'signature'. Example: signature = \"question -> answer\""
293
+ )
294
+
295
+ strategy = config.get("strategy", "predict")
296
+
297
+ # Extract optional input/output schemas
298
+ input_schema = config.get("input")
299
+ output_schema = config.get("output")
300
+
301
+ # Extract any additional kwargs (excluding known fields)
302
+ known_fields = {"signature", "strategy", "input", "output"}
303
+ extra_kwargs = {k: v for k, v in config.items() if k not in known_fields}
304
+
305
+ module = TactusModule(
306
+ name=name,
307
+ signature=signature,
308
+ strategy=strategy,
309
+ input_schema=input_schema,
310
+ output_schema=output_schema,
311
+ **extra_kwargs,
312
+ )
313
+
314
+ # Wrap in Lua-callable wrapper with mocking support
315
+ return LuaCallableModule(module, registry=registry, mock_manager=mock_manager)
316
+
317
+
318
+ class LuaCallableModule:
319
+ """
320
+ Wrapper that makes TactusModule callable from Lua with mocking support.
321
+
322
+ In Lua, you call a module like: qa({question = "What is 2+2?"})
323
+ This passes a table as a single positional argument.
324
+
325
+ This wrapper:
326
+ 1. Checks if the module is mocked (via Mocks {})
327
+ 2. If mocked, returns the mock response
328
+ 3. Otherwise, converts the input table to Python **kwargs for TactusModule.__call__
329
+ """
330
+
331
+ def __init__(self, module: TactusModule, registry: Any = None, mock_manager: Any = None):
332
+ self.module = module
333
+ self.registry = registry
334
+ self.mock_manager = mock_manager
335
+
336
+ @property
337
+ def signature(self):
338
+ """Expose the underlying module's signature for introspection."""
339
+ return self.module.signature
340
+
341
+ @property
342
+ def name(self):
343
+ """Expose the underlying module's name."""
344
+ return self.module.name
345
+
346
+ @property
347
+ def strategy(self):
348
+ """Expose the underlying module's strategy."""
349
+ return self.module.strategy
350
+
351
+ @property
352
+ def input_schema(self):
353
+ """Expose the underlying module's input schema."""
354
+ return self.module.input_schema
355
+
356
+ @property
357
+ def output_schema(self):
358
+ """Expose the underlying module's output schema."""
359
+ return self.module.output_schema
360
+
361
+ def __call__(self, inputs: Dict[str, Any]) -> Union[dspy.Prediction, Dict[str, Any]]:
362
+ """
363
+ Execute the module with inputs from a Lua table.
364
+
365
+ Args:
366
+ inputs: Dictionary of input values (from Lua table)
367
+
368
+ Returns:
369
+ A DSPy Prediction object or mock response dict
370
+ """
371
+ # Check for mock first
372
+ if self.mock_manager and self.registry:
373
+ mock_response = self._get_mock_response(inputs)
374
+ if mock_response is not None:
375
+ # Return mock response wrapped as a dict
376
+ # (DSPy Prediction fields are accessed as attributes, but we can return a dict from mocks)
377
+ return mock_response
378
+
379
+ # No mock - call real DSPy module
380
+ return self.module(**inputs)
381
+
382
+ def _get_mock_response(self, inputs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
383
+ """
384
+ Check if this module has a mock configured and return mock response.
385
+
386
+ Uses the same mock logic as tools: static (returns), temporal, and conditional.
387
+
388
+ Args:
389
+ inputs: The input arguments to the module
390
+
391
+ Returns:
392
+ Mock response dict if mocked, None otherwise
393
+ """
394
+ module_name = self.module.name
395
+
396
+ # Check if module has a mock in the registry
397
+ if module_name not in self.registry.mocks:
398
+ return None
399
+
400
+ # Use mock_manager to get the response (handles static/temporal/conditional logic)
401
+ try:
402
+ return self.mock_manager.get_mock_response(module_name, inputs)
403
+ except Exception:
404
+ # If mock_manager throws an error (e.g., error simulation), let it propagate
405
+ raise