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,318 @@
1
+ """
2
+ DSPy Prediction integration for Tactus.
3
+
4
+ This module provides the Prediction primitive that maps to DSPy Prediction,
5
+ representing the output of DSPy Module calls with convenient access methods.
6
+ """
7
+
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import dspy
11
+
12
+
13
+ class TactusPrediction:
14
+ """
15
+ A Tactus wrapper around DSPy Prediction.
16
+
17
+ This class provides a convenient API for accessing prediction results
18
+ from DSPy Modules. It wraps the native DSPy Prediction while adding
19
+ Tactus-specific convenience methods.
20
+
21
+ Attributes are accessible directly:
22
+ result = module(question="What is 2+2?")
23
+ print(result.answer) # Access output field
24
+
25
+ Example usage in Lua:
26
+ local result = qa_module({ question = "What is 2+2?" })
27
+
28
+ -- Access output fields
29
+ print(result.answer)
30
+
31
+ -- Get all output values as a table
32
+ local data = result.data()
33
+
34
+ -- Check if prediction has a specific field
35
+ if result.has("reasoning") then
36
+ print(result.reasoning)
37
+ end
38
+
39
+ -- Access conversation messages
40
+ local new_msgs = result.new_messages() -- Messages from this turn
41
+ local all_msgs = result.all_messages() -- All conversation messages
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ dspy_prediction: dspy.Prediction,
47
+ new_messages: Optional[List[Dict[str, Any]]] = None,
48
+ all_messages: Optional[List[Dict[str, Any]]] = None,
49
+ ):
50
+ """
51
+ Initialize a TactusPrediction from a DSPy Prediction.
52
+
53
+ Args:
54
+ dspy_prediction: The DSPy Prediction object to wrap
55
+ new_messages: Messages added during this turn (user + assistant)
56
+ all_messages: All messages in the conversation history
57
+ """
58
+ self._prediction = dspy_prediction
59
+ self._new_messages = new_messages or []
60
+ self._all_messages = all_messages or []
61
+
62
+ def __getattr__(self, name: str) -> Any:
63
+ """
64
+ Access prediction fields as attributes.
65
+
66
+ Delegates to the underlying DSPy Prediction.
67
+ """
68
+ if name.startswith("_"):
69
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
70
+ return getattr(self._prediction, name)
71
+
72
+ def data(self) -> Dict[str, Any]:
73
+ """
74
+ Get all prediction data as a dictionary.
75
+
76
+ Returns:
77
+ Dict containing all output fields and their values
78
+ """
79
+ return dict(self._prediction)
80
+
81
+ def has(self, field_name: str) -> bool:
82
+ """
83
+ Check if the prediction has a specific field.
84
+
85
+ Args:
86
+ field_name: The field to check for
87
+
88
+ Returns:
89
+ True if the field exists in the prediction
90
+ """
91
+ return hasattr(self._prediction, field_name)
92
+
93
+ def get(self, field_name: str, default: Any = None) -> Any:
94
+ """
95
+ Get a field value with a default if not present.
96
+
97
+ Args:
98
+ field_name: The field to get
99
+ default: Default value if field doesn't exist
100
+
101
+ Returns:
102
+ The field value or default
103
+ """
104
+ return getattr(self._prediction, field_name, default)
105
+
106
+ def to_dspy(self) -> dspy.Prediction:
107
+ """
108
+ Get the underlying DSPy Prediction.
109
+
110
+ Returns:
111
+ The wrapped dspy.Prediction object
112
+ """
113
+ return self._prediction
114
+
115
+ @classmethod
116
+ def from_dspy(cls, prediction: dspy.Prediction) -> "TactusPrediction":
117
+ """
118
+ Create a TactusPrediction from a DSPy Prediction.
119
+
120
+ Args:
121
+ prediction: A dspy.Prediction instance
122
+
123
+ Returns:
124
+ A TactusPrediction instance
125
+ """
126
+ return cls(prediction)
127
+
128
+ @property
129
+ def message(self) -> str:
130
+ """
131
+ Get the message content from the prediction.
132
+
133
+ This is a convenience property that tries common field names
134
+ for message content. Useful for accessing agent responses.
135
+
136
+ Returns:
137
+ The message content, or empty string if not found
138
+
139
+ Priority order:
140
+ 1. response (most common for agent responses)
141
+ 2. text
142
+ 3. answer
143
+ 4. content
144
+ 5. output
145
+ 6. First string field found
146
+ 7. Empty string if nothing found
147
+ """
148
+ # Try common field names in priority order
149
+ for field in ["response", "text", "answer", "content", "output"]:
150
+ value = getattr(self._prediction, field, None)
151
+ if value is not None and isinstance(value, str):
152
+ return value
153
+
154
+ # Fall back to first string value found
155
+ for key in dir(self._prediction):
156
+ if not key.startswith("_"):
157
+ value = getattr(self._prediction, key, None)
158
+ if value is not None and isinstance(value, str):
159
+ return value
160
+
161
+ return ""
162
+
163
+ def new_messages(self) -> List[Dict[str, Any]]:
164
+ """
165
+ Get messages that were added during this turn.
166
+
167
+ Returns a list of message dictionaries with 'role' and 'content' keys.
168
+ Typically includes the user message (if any) and the assistant's response.
169
+
170
+ Returns:
171
+ List of message dicts from this turn
172
+
173
+ Example:
174
+ result = agent({message = "Hello"})
175
+ msgs = result.new_messages()
176
+ -- msgs = [
177
+ -- {role = "user", content = "Hello"},
178
+ -- {role = "assistant", content = "Hi there!"}
179
+ -- ]
180
+ """
181
+ return self._new_messages.copy()
182
+
183
+ def all_messages(self) -> List[Dict[str, Any]]:
184
+ """
185
+ Get all messages in the conversation history.
186
+
187
+ Returns the complete conversation history including all previous turns
188
+ and the current turn.
189
+
190
+ Returns:
191
+ List of all message dicts in the conversation
192
+
193
+ Example:
194
+ result = agent({message = "What's next?"})
195
+ all_msgs = result.all_messages()
196
+ -- Returns all messages from the entire conversation
197
+ """
198
+ return self._all_messages.copy()
199
+
200
+
201
+ def validate_field_name(field_name: str) -> bool:
202
+ """
203
+ Validate prediction field name.
204
+
205
+ Args:
206
+ field_name: Field name to validate
207
+
208
+ Returns:
209
+ True if field name is valid, False otherwise
210
+ """
211
+ import re
212
+
213
+ # Field names must start with a letter or underscore, followed by
214
+ # optional letters, digits, or underscores
215
+ return re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", field_name) is not None
216
+
217
+
218
+ def validate_field_type(field_name: str, value: Any, schema: Dict[str, Any] = None) -> bool:
219
+ """
220
+ Validate prediction field type.
221
+
222
+ Args:
223
+ field_name: Name of the field
224
+ value: Value to validate
225
+ schema: Optional type schema
226
+
227
+ Returns:
228
+ True if field type is valid, False otherwise
229
+ """
230
+ # Default type validation if no schema provided
231
+ if schema is None:
232
+ return True
233
+
234
+ type_mapping = {
235
+ "str": str,
236
+ "int": int,
237
+ "float": float,
238
+ "bool": bool,
239
+ "list": list,
240
+ }
241
+
242
+ field_type = schema.get("fields", {}).get(field_name, {}).get("type")
243
+ if field_type:
244
+ expected_type = type_mapping.get(field_type)
245
+ return isinstance(value, expected_type) if expected_type else False
246
+
247
+ return True
248
+
249
+
250
+ def create_prediction(**kwargs: Any) -> TactusPrediction:
251
+ """
252
+ Create a new TactusPrediction directly.
253
+
254
+ This is useful for creating prediction objects manually,
255
+ e.g., in tests or when constructing results programmatically.
256
+
257
+ Args:
258
+ **kwargs: Field values for the prediction
259
+ Special keys:
260
+ - __schema__: Optional schema for validation
261
+ - __new_messages__: Messages from this turn
262
+ - __all_messages__: All conversation messages
263
+
264
+ Returns:
265
+ A TactusPrediction instance
266
+
267
+ Raises:
268
+ ValueError: For invalid field names or missing required fields
269
+ """
270
+ # Extract special message tracking keys
271
+ new_messages = kwargs.pop("__new_messages__", [])
272
+ all_messages = kwargs.pop("__all_messages__", [])
273
+
274
+ # Validate field names
275
+ for field in kwargs.keys():
276
+ if not validate_field_name(field):
277
+ raise ValueError(f"Invalid field name: {field}")
278
+
279
+ # Optional schema validation (can be injected via special key)
280
+ schema = kwargs.pop("__schema__", {}) if "__schema__" in kwargs else {}
281
+
282
+ # Validate required fields
283
+ required_fields = schema.get("required", [])
284
+ for field in required_fields:
285
+ if field not in kwargs:
286
+ raise ValueError(f"Required field missing: {field}")
287
+
288
+ # Validate field types
289
+ for field, value in kwargs.items():
290
+ if not validate_field_type(field, value, schema):
291
+ expected_type = schema.get("fields", {}).get(field, {}).get("type")
292
+ raise TypeError(
293
+ f"Field {field} type mismatch. Expected {expected_type}, got {type(value).__name__}"
294
+ )
295
+
296
+ # Create and return the Prediction
297
+ return TactusPrediction(
298
+ dspy.Prediction(**kwargs), new_messages=new_messages, all_messages=all_messages
299
+ )
300
+
301
+
302
+ def wrap_prediction(
303
+ dspy_prediction: dspy.Prediction,
304
+ new_messages: Optional[List[Dict[str, Any]]] = None,
305
+ all_messages: Optional[List[Dict[str, Any]]] = None,
306
+ ) -> TactusPrediction:
307
+ """
308
+ Wrap a DSPy Prediction in a TactusPrediction.
309
+
310
+ Args:
311
+ dspy_prediction: The DSPy Prediction to wrap
312
+ new_messages: Messages added during this turn
313
+ all_messages: All messages in the conversation history
314
+
315
+ Returns:
316
+ A TactusPrediction instance
317
+ """
318
+ return TactusPrediction(dspy_prediction, new_messages=new_messages, all_messages=all_messages)
@@ -0,0 +1,185 @@
1
+ """
2
+ DSPy Signature integration for Tactus.
3
+
4
+ This module provides the Signature primitive that maps to DSPy signatures,
5
+ supporting both string format ("question -> answer") and structured format.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, Union
9
+
10
+ import dspy
11
+
12
+
13
+ # Map Tactus types to Python types for DSPy fields
14
+ TYPE_MAP = {
15
+ "string": str,
16
+ "str": str,
17
+ "number": float,
18
+ "float": float,
19
+ "integer": int,
20
+ "int": int,
21
+ "boolean": bool,
22
+ "bool": bool,
23
+ "array": list,
24
+ "list": list,
25
+ "object": dict,
26
+ "dict": dict,
27
+ }
28
+
29
+
30
+ def parse_signature_string(sig_str: str) -> dspy.Signature:
31
+ """
32
+ Parse a DSPy-style signature string into a dspy.Signature.
33
+
34
+ DSPy 3.x natively supports parsing signature strings, so we delegate to it.
35
+
36
+ Signature string format:
37
+ - Simple: "question -> answer"
38
+ - Multi-field: "context, question -> reasoning, answer"
39
+ - Typed: "question: str -> answer: str"
40
+
41
+ Args:
42
+ sig_str: Signature string like "question -> answer"
43
+
44
+ Returns:
45
+ A dspy.Signature class
46
+
47
+ Raises:
48
+ ValueError: If the signature string is invalid
49
+ """
50
+ # Validate signature string
51
+ if not sig_str or not isinstance(sig_str, str):
52
+ raise ValueError("Signature string cannot be empty")
53
+
54
+ # Check for arrow
55
+ if "->" not in sig_str:
56
+ raise ValueError("Invalid signature format: must contain exactly one '->' separator")
57
+
58
+ parts = sig_str.split("->")
59
+ if len(parts) != 2:
60
+ raise ValueError("Invalid signature format: must contain exactly one '->' separator")
61
+
62
+ input_part = parts[0].strip()
63
+ output_part = parts[1].strip()
64
+
65
+ # Check for empty fields
66
+ if not input_part or not output_part:
67
+ raise ValueError("Signature cannot have empty fields on either side of '->'")
68
+
69
+ # Parse field names
70
+ input_fields = [f.strip() for f in input_part.split(",")]
71
+ output_fields = [f.strip() for f in output_part.split(",")]
72
+
73
+ # Check for empty field names
74
+ if any(not f for f in input_fields) or any(not f for f in output_fields):
75
+ raise ValueError("Signature cannot have empty fields")
76
+
77
+ # Check for duplicate field names
78
+ all_fields = input_fields + output_fields
79
+ if len(all_fields) != len(set(all_fields)):
80
+ duplicates = [f for f in all_fields if all_fields.count(f) > 1]
81
+ raise ValueError(f"Signature contains duplicate field names: {', '.join(set(duplicates))}")
82
+
83
+ # DSPy 3.x can parse signature strings directly
84
+ return dspy.Signature(sig_str)
85
+
86
+
87
+ def create_structured_signature(
88
+ input_fields: Dict[str, Dict[str, Any]],
89
+ output_fields: Dict[str, Dict[str, Any]],
90
+ name: Optional[str] = None,
91
+ instructions: Optional[str] = None,
92
+ ) -> dspy.Signature:
93
+ """
94
+ Create a DSPy Signature from structured field definitions.
95
+
96
+ This allows defining signatures with descriptions and types using
97
+ Tactus's field.string{}, field.number{} etc. syntax.
98
+
99
+ Args:
100
+ input_fields: Dict mapping field names to their definitions
101
+ e.g., {"question": {"type": "string", "description": "The question"}}
102
+ output_fields: Dict mapping field names to their definitions
103
+ e.g., {"answer": {"type": "string", "description": "The answer"}}
104
+ name: Optional name for the signature class
105
+ instructions: Optional instructions/docstring for the signature
106
+
107
+ Returns:
108
+ A dspy.Signature class with the specified fields and descriptions
109
+ """
110
+ # Build field names for the string signature
111
+ input_names = list(input_fields.keys())
112
+ output_names = list(output_fields.keys())
113
+
114
+ # Create base signature string
115
+ sig_str = f"{', '.join(input_names)} -> {', '.join(output_names)}"
116
+
117
+ # Create the base signature
118
+ sig = dspy.Signature(sig_str)
119
+
120
+ # Update each field with its description using with_updated_fields
121
+ # DSPy's with_updated_fields takes one field name at a time
122
+ for field_name, field_def in input_fields.items():
123
+ desc = field_def.get("description", "")
124
+ if desc:
125
+ sig = sig.with_updated_fields(field_name, desc=desc)
126
+
127
+ for field_name, field_def in output_fields.items():
128
+ desc = field_def.get("description", "")
129
+ if desc:
130
+ sig = sig.with_updated_fields(field_name, desc=desc)
131
+
132
+ # Add instructions if provided
133
+ if instructions:
134
+ sig = sig.with_instructions(instructions)
135
+
136
+ return sig
137
+
138
+
139
+ def create_signature(
140
+ sig_input: Union[str, Dict[str, Any]],
141
+ name: Optional[str] = None,
142
+ ) -> dspy.Signature:
143
+ """
144
+ Create a DSPy Signature from string or structured input.
145
+
146
+ Args:
147
+ sig_input: Either a string like "question -> answer" or a dict with
148
+ input/output field definitions
149
+ name: Optional name for the signature (used in structured form)
150
+
151
+ Returns:
152
+ A dspy.Signature class
153
+
154
+ Examples:
155
+ # String form
156
+ create_signature("question -> answer")
157
+ create_signature("context, question -> reasoning, answer")
158
+
159
+ # Structured form
160
+ create_signature({
161
+ "input": {"question": {"type": "string", "description": "The question"}},
162
+ "output": {"answer": {"type": "string", "description": "The answer"}}
163
+ })
164
+ """
165
+ if isinstance(sig_input, str):
166
+ return parse_signature_string(sig_input)
167
+ elif isinstance(sig_input, dict):
168
+ # Structured form
169
+ input_fields = sig_input.get("input", {})
170
+ output_fields = sig_input.get("output", {})
171
+ instructions = sig_input.get("instructions")
172
+
173
+ if not input_fields and not output_fields:
174
+ raise ValueError(
175
+ "Structured signature must have 'input' and/or 'output' field definitions"
176
+ )
177
+
178
+ return create_structured_signature(
179
+ input_fields=input_fields,
180
+ output_fields=output_fields,
181
+ name=name,
182
+ instructions=instructions,
183
+ )
184
+ else:
185
+ raise TypeError(f"Signature expects a string or dict, got {type(sig_input).__name__}")
@@ -0,0 +1,7 @@
1
+ """
2
+ Formatting utilities for Tactus Lua DSL files.
3
+ """
4
+
5
+ from .formatter import TactusFormatter, FormattingError
6
+
7
+ __all__ = ["TactusFormatter", "FormattingError"]