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
|
@@ -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)
|
tactus/dspy/signature.py
ADDED
|
@@ -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__}")
|