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.
- 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.0.dist-info/METADATA +1809 -0
- tactus-0.31.0.dist-info/RECORD +160 -0
- tactus-0.31.0.dist-info/WHEEL +4 -0
- tactus-0.31.0.dist-info/entry_points.txt +2 -0
- tactus-0.31.0.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
|