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.
- 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.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lua-callable wrapper for named sub-procedures.
|
|
3
|
+
|
|
4
|
+
This module provides the ProcedureCallable class that enables direct function
|
|
5
|
+
call syntax for named procedures with automatic checkpointing and replay support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProcedureCallable:
|
|
12
|
+
"""
|
|
13
|
+
Lua-callable wrapper for named sub-procedures.
|
|
14
|
+
|
|
15
|
+
Enables direct function call syntax: result = my_proc({input})
|
|
16
|
+
Integrates with checkpoint system for auto-replay.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
helper = procedure("helper", {
|
|
20
|
+
input = {x = {type = "number"}},
|
|
21
|
+
output = {y = {type = "number"}}
|
|
22
|
+
}, function()
|
|
23
|
+
return {y = input.x * 2}
|
|
24
|
+
end)
|
|
25
|
+
|
|
26
|
+
-- Call it directly:
|
|
27
|
+
result = helper({x = 10}) -- Returns {y = 20}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
procedure_function: Any, # Lua function reference
|
|
34
|
+
input_schema: Dict[str, Any],
|
|
35
|
+
output_schema: Dict[str, Any],
|
|
36
|
+
state_schema: Dict[str, Any],
|
|
37
|
+
execution_context, # ExecutionContext instance
|
|
38
|
+
lua_sandbox, # LuaSandbox instance
|
|
39
|
+
is_main: bool = False, # Whether this is the main entry procedure
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Initialize a callable procedure wrapper.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
name: Procedure name
|
|
46
|
+
procedure_function: Lua function reference to execute
|
|
47
|
+
input_schema: Input validation schema
|
|
48
|
+
output_schema: Output validation schema
|
|
49
|
+
state_schema: State initialization schema
|
|
50
|
+
execution_context: ExecutionContext for checkpointing
|
|
51
|
+
lua_sandbox: LuaSandbox for Lua global management
|
|
52
|
+
is_main: If True, don't checkpoint (main is entry point)
|
|
53
|
+
"""
|
|
54
|
+
self.name = name
|
|
55
|
+
self.procedure_function = procedure_function
|
|
56
|
+
self.input_schema = input_schema
|
|
57
|
+
self.output_schema = output_schema
|
|
58
|
+
self.state_schema = state_schema
|
|
59
|
+
self.execution_context = execution_context
|
|
60
|
+
self.lua_sandbox = lua_sandbox
|
|
61
|
+
self.is_main = is_main
|
|
62
|
+
|
|
63
|
+
def __call__(self, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Execute the sub-procedure when called from Lua.
|
|
66
|
+
|
|
67
|
+
This method is invoked when Lua code calls: result = my_proc({...})
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
params: Input parameters as a dictionary (converted from Lua table)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The procedure's result (will be converted to Lua table)
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If input validation fails or output is missing required fields
|
|
77
|
+
"""
|
|
78
|
+
params = params or {}
|
|
79
|
+
|
|
80
|
+
# Convert Lua table to dict if needed
|
|
81
|
+
if hasattr(params, "items"):
|
|
82
|
+
from tactus.core.dsl_stubs import lua_table_to_dict
|
|
83
|
+
|
|
84
|
+
params = lua_table_to_dict(params)
|
|
85
|
+
|
|
86
|
+
# Handle empty list case (lua_table_to_dict converts empty {} to [])
|
|
87
|
+
if isinstance(params, list) and len(params) == 0:
|
|
88
|
+
params = {}
|
|
89
|
+
|
|
90
|
+
# Validate input against schema
|
|
91
|
+
self._validate_input(params)
|
|
92
|
+
|
|
93
|
+
# Wrap execution in checkpoint for automatic replay
|
|
94
|
+
def execute_procedure():
|
|
95
|
+
# Convert Python lists/dicts to Lua tables before setting as input
|
|
96
|
+
def convert_to_lua(value):
|
|
97
|
+
"""Recursively convert Python lists and dicts to Lua tables."""
|
|
98
|
+
if isinstance(value, list):
|
|
99
|
+
# Convert Python list to Lua table (1-indexed)
|
|
100
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
101
|
+
for i, item in enumerate(value, 1):
|
|
102
|
+
lua_table[i] = convert_to_lua(item)
|
|
103
|
+
return lua_table
|
|
104
|
+
elif isinstance(value, dict):
|
|
105
|
+
# Convert Python dict to Lua table
|
|
106
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
107
|
+
for k, v in value.items():
|
|
108
|
+
lua_table[k] = convert_to_lua(v)
|
|
109
|
+
return lua_table
|
|
110
|
+
else:
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
# Convert params to Lua-compatible format
|
|
114
|
+
lua_params = self.lua_sandbox.lua.table()
|
|
115
|
+
for key, value in params.items():
|
|
116
|
+
lua_params[key] = convert_to_lua(value)
|
|
117
|
+
|
|
118
|
+
# Initialize state defaults WITHOUT replacing the state table
|
|
119
|
+
# (preserving the metatable setup)
|
|
120
|
+
state_defaults = self._initialize_state()
|
|
121
|
+
if state_defaults:
|
|
122
|
+
# Access state via globals and assign - this will trigger the metatable
|
|
123
|
+
state_table = self.lua_sandbox.lua.globals()["state"]
|
|
124
|
+
for key, value in state_defaults.items():
|
|
125
|
+
state_table[key] = convert_to_lua(value)
|
|
126
|
+
|
|
127
|
+
# Execute the procedure function with input as explicit parameter
|
|
128
|
+
result = self.procedure_function(lua_params)
|
|
129
|
+
|
|
130
|
+
# Convert Lua table result to Python dict
|
|
131
|
+
# Check for lupa table (not Python dict/list)
|
|
132
|
+
if result and hasattr(result, "items") and not isinstance(result, (dict, list)):
|
|
133
|
+
from tactus.core.dsl_stubs import lua_table_to_dict
|
|
134
|
+
|
|
135
|
+
result = lua_table_to_dict(result)
|
|
136
|
+
|
|
137
|
+
# Validate output
|
|
138
|
+
self._validate_output(result)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
# Use existing checkpoint infrastructure for sub-procedures
|
|
143
|
+
# Main procedure is NOT checkpointed (it's the entry point)
|
|
144
|
+
if self.is_main:
|
|
145
|
+
# Main procedure: execute directly without checkpointing
|
|
146
|
+
return execute_procedure()
|
|
147
|
+
else:
|
|
148
|
+
# Sub-procedure: checkpoint for automatic replay
|
|
149
|
+
# Try to capture Lua source location if available
|
|
150
|
+
source_info = None
|
|
151
|
+
|
|
152
|
+
# First try to get Lua debug info
|
|
153
|
+
# When called from Lua, we need to find the Lua caller's location
|
|
154
|
+
try:
|
|
155
|
+
# Get debug.getinfo function from Lua globals
|
|
156
|
+
lua_globals = self.lua_sandbox.lua.globals()
|
|
157
|
+
if hasattr(lua_globals, "debug") and hasattr(lua_globals.debug, "getinfo"):
|
|
158
|
+
# Try different stack levels to find the Lua caller
|
|
159
|
+
debug_info = None
|
|
160
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
161
|
+
f.write(f"DEBUG: Trying debug.getinfo for {self.name}\n")
|
|
162
|
+
for level in [1, 2, 3, 4, 5, 6, 7, 8]:
|
|
163
|
+
try:
|
|
164
|
+
info = lua_globals.debug.getinfo(level, "Sl")
|
|
165
|
+
if info:
|
|
166
|
+
lua_dict = dict(info.items()) if hasattr(info, "items") else {}
|
|
167
|
+
source = lua_dict.get("source", "")
|
|
168
|
+
line = lua_dict.get("currentline", -1)
|
|
169
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
170
|
+
f.write(f"DEBUG: Level {level}: source={source}, line={line}\n")
|
|
171
|
+
# Look for a valid source location (not -1, not C function)
|
|
172
|
+
# Accept [string "<python>"] sources since that's our Lua code
|
|
173
|
+
if line > 0 and source:
|
|
174
|
+
if source.startswith("=[C]"):
|
|
175
|
+
continue # Skip C functions
|
|
176
|
+
debug_info = lua_dict
|
|
177
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
178
|
+
f.write(f"DEBUG: Found valid source at level {level}\n")
|
|
179
|
+
break
|
|
180
|
+
except Exception as inner_e:
|
|
181
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
182
|
+
f.write(f"DEBUG: Level {level} error: {inner_e}\n")
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if debug_info:
|
|
186
|
+
source_info = {
|
|
187
|
+
"file": self.execution_context.current_tac_file
|
|
188
|
+
or debug_info.get("source", "unknown"),
|
|
189
|
+
"line": debug_info.get("currentline", 0),
|
|
190
|
+
"function": debug_info.get("name", self.name),
|
|
191
|
+
}
|
|
192
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
193
|
+
f.write(f"DEBUG: Final source_info: {source_info}\n")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
196
|
+
f.write(f"DEBUG: Exception getting Lua debug info: {e}\n")
|
|
197
|
+
|
|
198
|
+
# If we still don't have source_info, use fallback
|
|
199
|
+
if not source_info:
|
|
200
|
+
import inspect
|
|
201
|
+
|
|
202
|
+
frame = inspect.currentframe()
|
|
203
|
+
if frame and frame.f_back:
|
|
204
|
+
caller_frame = frame.f_back
|
|
205
|
+
# Use .tac file if available, otherwise use Python file
|
|
206
|
+
tac_file = self.execution_context.current_tac_file
|
|
207
|
+
python_file = caller_frame.f_code.co_filename
|
|
208
|
+
with open("/tmp/tactus-debug.log", "a") as f:
|
|
209
|
+
f.write(
|
|
210
|
+
f"DEBUG: Fallback - current_tac_file={tac_file}, python_file={python_file}\n"
|
|
211
|
+
)
|
|
212
|
+
source_info = {
|
|
213
|
+
"file": tac_file or python_file,
|
|
214
|
+
"line": 0, # Line number unknown without Lua debug
|
|
215
|
+
"function": self.name,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return self.execution_context.checkpoint(
|
|
219
|
+
execute_procedure, checkpoint_type="procedure_call", source_info=source_info
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _validate_input(self, params: Dict[str, Any]) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Validate input parameters against input schema.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
params: Input parameters to validate
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ValueError: If required fields are missing
|
|
231
|
+
"""
|
|
232
|
+
missing_inputs = []
|
|
233
|
+
for field_name, field_def in self.input_schema.items():
|
|
234
|
+
if isinstance(field_def, dict) and field_def.get("required", False):
|
|
235
|
+
if field_name not in params:
|
|
236
|
+
field_type = field_def.get("type", "any")
|
|
237
|
+
field_desc = field_def.get("description", "")
|
|
238
|
+
missing_inputs.append(
|
|
239
|
+
f" - {field_name} ({field_type}): {field_desc}"
|
|
240
|
+
if field_desc
|
|
241
|
+
else f" - {field_name} ({field_type})"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if missing_inputs:
|
|
245
|
+
inputs_list = "\n".join(missing_inputs)
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"Procedure '{self.name}' requires input parameters that were not provided:\n{inputs_list}\n\n"
|
|
248
|
+
f"To run this procedure, provide the required inputs via the API or use a test specification."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _validate_output(self, result: Any) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Validate output against output schema.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
result: Output to validate
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ValueError: If output is not a dict or missing required fields
|
|
260
|
+
"""
|
|
261
|
+
# If no output schema is declared, accept any return value.
|
|
262
|
+
if not self.output_schema:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Scalar output schema support:
|
|
266
|
+
# output = field.string{...}
|
|
267
|
+
if (
|
|
268
|
+
isinstance(self.output_schema, dict)
|
|
269
|
+
and "type" in self.output_schema
|
|
270
|
+
and isinstance(self.output_schema.get("type"), str)
|
|
271
|
+
):
|
|
272
|
+
expected_type = self.output_schema.get("type")
|
|
273
|
+
if expected_type not in {"string", "number", "boolean", "object", "array"}:
|
|
274
|
+
# Not a scalar schema; treat as normal object schema.
|
|
275
|
+
expected_type = None
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
expected_type = None
|
|
279
|
+
|
|
280
|
+
if expected_type is not None:
|
|
281
|
+
is_required = bool(self.output_schema.get("required", False))
|
|
282
|
+
if result is None and not is_required:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
if expected_type == "string" and not isinstance(result, str):
|
|
286
|
+
raise ValueError(f"Procedure '{self.name}' must return string, got {type(result)}")
|
|
287
|
+
if expected_type == "number" and not isinstance(result, (int, float)):
|
|
288
|
+
raise ValueError(f"Procedure '{self.name}' must return number, got {type(result)}")
|
|
289
|
+
if expected_type == "boolean" and not isinstance(result, bool):
|
|
290
|
+
raise ValueError(f"Procedure '{self.name}' must return boolean, got {type(result)}")
|
|
291
|
+
if expected_type == "object" and not isinstance(result, dict):
|
|
292
|
+
raise ValueError(f"Procedure '{self.name}' must return object, got {type(result)}")
|
|
293
|
+
if expected_type == "array" and not isinstance(result, list):
|
|
294
|
+
raise ValueError(f"Procedure '{self.name}' must return array, got {type(result)}")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if not isinstance(result, dict):
|
|
298
|
+
raise ValueError(f"Procedure '{self.name}' must return dict, got {type(result)}")
|
|
299
|
+
|
|
300
|
+
for field_name, field_def in self.output_schema.items():
|
|
301
|
+
if isinstance(field_def, dict) and field_def.get("required", False):
|
|
302
|
+
if field_name not in result:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"Procedure '{self.name}' missing required output: {field_name}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def _initialize_state(self) -> Dict[str, Any]:
|
|
308
|
+
"""
|
|
309
|
+
Initialize state with default values from state schema.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Dictionary with state defaults
|
|
313
|
+
"""
|
|
314
|
+
state = {}
|
|
315
|
+
for field_name, field_def in self.state_schema.items():
|
|
316
|
+
if isinstance(field_def, dict) and "default" in field_def:
|
|
317
|
+
state[field_name] = field_def["default"]
|
|
318
|
+
return state
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retry Primitive - Error handling with exponential backoff.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Retry.with_backoff(fn, options) - Retry function with exponential backoff
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Callable, Any, Optional, Dict
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RetryPrimitive:
|
|
16
|
+
"""
|
|
17
|
+
Handles retry logic with exponential backoff for procedures.
|
|
18
|
+
|
|
19
|
+
Enables workflows to:
|
|
20
|
+
- Retry failed operations automatically
|
|
21
|
+
- Use exponential backoff between attempts
|
|
22
|
+
- Handle transient errors gracefully
|
|
23
|
+
- Configure max attempts and delays
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize Retry primitive."""
|
|
28
|
+
logger.debug("RetryPrimitive initialized")
|
|
29
|
+
|
|
30
|
+
def with_backoff(self, fn: Callable, options: Optional[Dict[str, Any]] = None) -> Any:
|
|
31
|
+
"""
|
|
32
|
+
Retry a function with exponential backoff.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
fn: Function to retry (Lua function)
|
|
36
|
+
options: Dict with:
|
|
37
|
+
- max_attempts: Maximum retry attempts (default: 3)
|
|
38
|
+
- initial_delay: Initial delay in seconds (default: 1)
|
|
39
|
+
- max_delay: Maximum delay in seconds (default: 60)
|
|
40
|
+
- backoff_factor: Multiplier for delay (default: 2)
|
|
41
|
+
- on_error: Optional callback when error occurs
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Result from successful function call
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
Exception: If all retry attempts fail
|
|
48
|
+
|
|
49
|
+
Example (Lua):
|
|
50
|
+
local result = Retry.with_backoff(function()
|
|
51
|
+
-- Try to fetch data from API
|
|
52
|
+
local data = fetch_api_data()
|
|
53
|
+
if not data then
|
|
54
|
+
error("API returned no data")
|
|
55
|
+
end
|
|
56
|
+
return data
|
|
57
|
+
end, {
|
|
58
|
+
max_attempts = 5,
|
|
59
|
+
initial_delay = 2,
|
|
60
|
+
backoff_factor = 2
|
|
61
|
+
})
|
|
62
|
+
"""
|
|
63
|
+
# Convert Lua tables to Python dicts if needed
|
|
64
|
+
opts = self._convert_lua_to_python(options) or {}
|
|
65
|
+
|
|
66
|
+
max_attempts = opts.get("max_attempts", 3)
|
|
67
|
+
initial_delay = opts.get("initial_delay", 1.0)
|
|
68
|
+
max_delay = opts.get("max_delay", 60.0)
|
|
69
|
+
backoff_factor = opts.get("backoff_factor", 2.0)
|
|
70
|
+
on_error = opts.get("on_error")
|
|
71
|
+
|
|
72
|
+
attempt = 0
|
|
73
|
+
delay = initial_delay
|
|
74
|
+
last_error = None
|
|
75
|
+
|
|
76
|
+
logger.info(f"Starting retry with_backoff (max_attempts={max_attempts})")
|
|
77
|
+
|
|
78
|
+
while attempt < max_attempts:
|
|
79
|
+
attempt += 1
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
logger.debug(f"Retry attempt {attempt}/{max_attempts}")
|
|
83
|
+
result = fn()
|
|
84
|
+
logger.info(f"Success on attempt {attempt}/{max_attempts}")
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
last_error = e
|
|
89
|
+
logger.warning(f"Attempt {attempt}/{max_attempts} failed: {e}")
|
|
90
|
+
|
|
91
|
+
# Call error callback if provided
|
|
92
|
+
if on_error and callable(on_error):
|
|
93
|
+
try:
|
|
94
|
+
on_error(
|
|
95
|
+
{
|
|
96
|
+
"attempt": attempt,
|
|
97
|
+
"max_attempts": max_attempts,
|
|
98
|
+
"error": str(e),
|
|
99
|
+
"delay": delay,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
except Exception as callback_error:
|
|
103
|
+
logger.error(f"Error callback failed: {callback_error}")
|
|
104
|
+
|
|
105
|
+
# Check if we should retry
|
|
106
|
+
if attempt >= max_attempts:
|
|
107
|
+
logger.error(f"All {max_attempts} attempts failed")
|
|
108
|
+
raise Exception(f"Retry failed after {max_attempts} attempts: {last_error}")
|
|
109
|
+
|
|
110
|
+
# Wait with exponential backoff
|
|
111
|
+
logger.info(f"Waiting {delay:.2f}s before retry...")
|
|
112
|
+
time.sleep(delay)
|
|
113
|
+
|
|
114
|
+
# Increase delay for next attempt (exponential backoff)
|
|
115
|
+
delay = min(delay * backoff_factor, max_delay)
|
|
116
|
+
|
|
117
|
+
# Should not reach here, but handle it
|
|
118
|
+
raise Exception(f"Retry logic error: {last_error}")
|
|
119
|
+
|
|
120
|
+
def _convert_lua_to_python(self, value: Any) -> Any:
|
|
121
|
+
"""
|
|
122
|
+
Recursively convert Lua tables to Python dicts.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
value: Lua value to convert
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Python equivalent (dict or primitive)
|
|
129
|
+
"""
|
|
130
|
+
if value is None:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Import lupa for table checking
|
|
134
|
+
try:
|
|
135
|
+
from lupa import lua_type
|
|
136
|
+
|
|
137
|
+
# Check if it's a Lua table
|
|
138
|
+
if lua_type(value) == "table":
|
|
139
|
+
result = {}
|
|
140
|
+
for k, v in value.items():
|
|
141
|
+
# Convert key and value recursively
|
|
142
|
+
py_key = self._convert_lua_to_python(k) if lua_type(k) == "table" else k
|
|
143
|
+
py_value = self._convert_lua_to_python(v)
|
|
144
|
+
result[py_key] = py_value
|
|
145
|
+
return result
|
|
146
|
+
else:
|
|
147
|
+
# Primitive value or function
|
|
148
|
+
return value
|
|
149
|
+
|
|
150
|
+
except ImportError:
|
|
151
|
+
# If lupa not available, just return as-is
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
return "RetryPrimitive()"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session primitive for managing conversation history.
|
|
3
|
+
|
|
4
|
+
Provides Lua-accessible methods for manipulating chat session state.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from pydantic_ai.messages import ModelMessage, ModelRequest, ModelResponse, TextPart
|
|
11
|
+
except ImportError:
|
|
12
|
+
# Fallback types if pydantic_ai not available
|
|
13
|
+
ModelMessage = dict
|
|
14
|
+
ModelRequest = dict
|
|
15
|
+
ModelResponse = dict
|
|
16
|
+
TextPart = dict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SessionPrimitive:
|
|
20
|
+
"""
|
|
21
|
+
Primitive for managing conversation session state.
|
|
22
|
+
|
|
23
|
+
Provides methods to:
|
|
24
|
+
- Append messages to history
|
|
25
|
+
- Inject system messages
|
|
26
|
+
- Clear history
|
|
27
|
+
- Access full history
|
|
28
|
+
- Save/load session state
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, session_manager=None, agent_name: Optional[str] = None):
|
|
32
|
+
"""
|
|
33
|
+
Initialize Session primitive.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
session_manager: SessionManager instance
|
|
37
|
+
agent_name: Name of the agent this session belongs to
|
|
38
|
+
"""
|
|
39
|
+
self.session_manager = session_manager
|
|
40
|
+
self.agent_name = agent_name
|
|
41
|
+
|
|
42
|
+
def append(self, message_data: dict) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Append a message to the session history.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
message_data: Dict with 'role' and 'content' keys
|
|
48
|
+
role: 'user', 'assistant', 'system'
|
|
49
|
+
content: message text
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
Session.append({role = "user", content = "Hello"})
|
|
53
|
+
"""
|
|
54
|
+
if not self.session_manager or not self.agent_name:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
role = message_data.get("role", "user")
|
|
58
|
+
content = message_data.get("content", "")
|
|
59
|
+
|
|
60
|
+
# Create a simple message dict
|
|
61
|
+
message = {"role": role, "content": content}
|
|
62
|
+
|
|
63
|
+
self.session_manager.add_message(self.agent_name, message)
|
|
64
|
+
|
|
65
|
+
def inject_system(self, text: str) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Inject a system message into the session.
|
|
68
|
+
|
|
69
|
+
This is useful for providing context or instructions
|
|
70
|
+
for the next agent turn.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
text: System message content
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
Session.inject_system("Focus on security implications")
|
|
77
|
+
"""
|
|
78
|
+
self.append({"role": "system", "content": text})
|
|
79
|
+
|
|
80
|
+
def clear(self) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Clear the session history for this agent.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
Session.clear()
|
|
86
|
+
"""
|
|
87
|
+
if not self.session_manager or not self.agent_name:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.session_manager.clear_agent_history(self.agent_name)
|
|
91
|
+
|
|
92
|
+
def history(self) -> list:
|
|
93
|
+
"""
|
|
94
|
+
Get the full conversation history for this agent.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of message dicts with 'role' and 'content' keys
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
local messages = Session.history()
|
|
101
|
+
for i, msg in ipairs(messages) do
|
|
102
|
+
Log.info(msg.role .. ": " .. msg.content)
|
|
103
|
+
end
|
|
104
|
+
"""
|
|
105
|
+
if not self.session_manager or not self.agent_name:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
messages = self.session_manager.histories.get(self.agent_name, [])
|
|
109
|
+
|
|
110
|
+
# Convert to Lua-friendly format
|
|
111
|
+
result = []
|
|
112
|
+
for msg in messages:
|
|
113
|
+
if isinstance(msg, dict):
|
|
114
|
+
result.append({"role": msg.get("role", ""), "content": str(msg.get("content", ""))})
|
|
115
|
+
else:
|
|
116
|
+
# Handle pydantic_ai ModelMessage objects
|
|
117
|
+
try:
|
|
118
|
+
result.append(
|
|
119
|
+
{
|
|
120
|
+
"role": getattr(msg, "role", ""),
|
|
121
|
+
"content": str(getattr(msg, "content", "")),
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
except Exception:
|
|
125
|
+
# Fallback: convert to string
|
|
126
|
+
result.append({"role": "unknown", "content": str(msg)})
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def load_from_node(self, node: Any) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Load session state from a graph node.
|
|
133
|
+
|
|
134
|
+
Not yet implemented - placeholder for future graph support.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
node: Graph node containing saved session state
|
|
138
|
+
"""
|
|
139
|
+
# TODO: Implement when graph primitives are added
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def save_to_node(self, node: Any) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Save session state to a graph node.
|
|
145
|
+
|
|
146
|
+
Not yet implemented - placeholder for future graph support.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
node: Graph node to save session state to
|
|
150
|
+
"""
|
|
151
|
+
# TODO: Implement when graph primitives are added
|
|
152
|
+
pass
|