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,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Human Primitive - Human-in-the-Loop (HITL) operations.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Human.approve(opts) - Request yes/no approval (blocking)
|
|
6
|
+
- Human.input(opts) - Request free-form input (blocking)
|
|
7
|
+
- Human.review(opts) - Request review with options (blocking)
|
|
8
|
+
- Human.notify(opts) - Send notification (non-blocking)
|
|
9
|
+
- Human.escalate(opts) - Escalate to human (blocking)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HumanPrimitive:
|
|
20
|
+
"""
|
|
21
|
+
Manages human-in-the-loop operations for procedures.
|
|
22
|
+
|
|
23
|
+
Uses a pluggable HITLHandler protocol implementation to manage
|
|
24
|
+
actual human interactions (via CLI, web UI, API, etc.).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, execution_context, hitl_config: Optional[Dict[str, Any]] = None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize Human primitive.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
execution_context: ExecutionContext with HITL handler
|
|
33
|
+
hitl_config: Optional HITL declarations from YAML
|
|
34
|
+
"""
|
|
35
|
+
self.execution_context = execution_context
|
|
36
|
+
self.hitl_config = hitl_config or {}
|
|
37
|
+
logger.debug("HumanPrimitive initialized")
|
|
38
|
+
|
|
39
|
+
def _convert_lua_to_python(self, obj: Any) -> Any:
|
|
40
|
+
"""Recursively convert Lua tables to Python dicts."""
|
|
41
|
+
if obj is None:
|
|
42
|
+
return None
|
|
43
|
+
# Check if it's a Lua table (has .items() but not a dict)
|
|
44
|
+
if hasattr(obj, "items") and not isinstance(obj, dict):
|
|
45
|
+
# Convert Lua table to dict
|
|
46
|
+
result = {}
|
|
47
|
+
for key, value in obj.items():
|
|
48
|
+
result[key] = self._convert_lua_to_python(value)
|
|
49
|
+
return result
|
|
50
|
+
elif isinstance(obj, dict):
|
|
51
|
+
# Recursively convert nested dicts
|
|
52
|
+
return {k: self._convert_lua_to_python(v) for k, v in obj.items()}
|
|
53
|
+
elif isinstance(obj, (list, tuple)):
|
|
54
|
+
# Recursively convert lists
|
|
55
|
+
return [self._convert_lua_to_python(item) for item in obj]
|
|
56
|
+
else:
|
|
57
|
+
# Primitive type, return as-is
|
|
58
|
+
return obj
|
|
59
|
+
|
|
60
|
+
def approve(self, options: Optional[Dict[str, Any]] = None) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Request yes/no approval from human (BLOCKING).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
options: Dict with:
|
|
66
|
+
- message: str - Message to show human
|
|
67
|
+
- context: Dict - Additional context
|
|
68
|
+
- timeout: int - Timeout in seconds (None = no timeout)
|
|
69
|
+
- default: bool - Default if timeout (default: False)
|
|
70
|
+
- config_key: str - Reference to hitl: declaration
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bool - True if approved, False if rejected/timeout
|
|
74
|
+
|
|
75
|
+
Example (Lua):
|
|
76
|
+
local approved = Human.approve({
|
|
77
|
+
message = "Deploy to production?",
|
|
78
|
+
context = {environment = "prod"},
|
|
79
|
+
timeout = 3600,
|
|
80
|
+
default = false
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if approved then
|
|
84
|
+
deploy()
|
|
85
|
+
end
|
|
86
|
+
"""
|
|
87
|
+
# Convert Lua tables to Python dicts recursively
|
|
88
|
+
opts = self._convert_lua_to_python(options) or {}
|
|
89
|
+
|
|
90
|
+
# Check for config reference
|
|
91
|
+
config_key = opts.get("config_key")
|
|
92
|
+
if config_key and config_key in self.hitl_config:
|
|
93
|
+
# Merge config with runtime options (runtime wins)
|
|
94
|
+
config_opts = self.hitl_config[config_key].copy()
|
|
95
|
+
config_opts.update(opts)
|
|
96
|
+
opts = config_opts
|
|
97
|
+
|
|
98
|
+
message = opts.get("message", "Approval requested")
|
|
99
|
+
context = opts.get("context", {})
|
|
100
|
+
timeout = opts.get("timeout")
|
|
101
|
+
default = opts.get("default", False)
|
|
102
|
+
|
|
103
|
+
logger.info(f"Human approval requested: {message[:50]}...")
|
|
104
|
+
|
|
105
|
+
# Delegate to execution context's wait_for_human
|
|
106
|
+
response = self.execution_context.wait_for_human(
|
|
107
|
+
request_type="approval",
|
|
108
|
+
message=message,
|
|
109
|
+
timeout_seconds=timeout,
|
|
110
|
+
default_value=default,
|
|
111
|
+
options=None,
|
|
112
|
+
metadata=context,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return response.value
|
|
116
|
+
|
|
117
|
+
def input(self, options: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
|
118
|
+
"""
|
|
119
|
+
Request free-form input from human (BLOCKING).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
options: Dict with:
|
|
123
|
+
- message: str - Prompt for human
|
|
124
|
+
- placeholder: str - Input placeholder
|
|
125
|
+
- timeout: int - Timeout in seconds
|
|
126
|
+
- default: str - Default if timeout
|
|
127
|
+
- config_key: str - Reference to hitl: declaration
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
str or None - Human's input, or None if timeout with no default
|
|
131
|
+
|
|
132
|
+
Example (Lua):
|
|
133
|
+
local topic = Human.input({
|
|
134
|
+
message = "What topic?",
|
|
135
|
+
placeholder = "Enter topic...",
|
|
136
|
+
timeout = 600
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if topic then
|
|
140
|
+
State.set("topic", topic)
|
|
141
|
+
end
|
|
142
|
+
"""
|
|
143
|
+
# Convert Lua table to dict if needed
|
|
144
|
+
opts = self._convert_lua_to_python(options) or {}
|
|
145
|
+
|
|
146
|
+
# Check for config reference
|
|
147
|
+
config_key = opts.get("config_key")
|
|
148
|
+
if config_key and config_key in self.hitl_config:
|
|
149
|
+
config_opts = self.hitl_config[config_key].copy()
|
|
150
|
+
config_opts.update(opts)
|
|
151
|
+
opts = config_opts
|
|
152
|
+
|
|
153
|
+
message = opts.get("message", "Input requested")
|
|
154
|
+
placeholder = opts.get("placeholder", "")
|
|
155
|
+
timeout = opts.get("timeout")
|
|
156
|
+
default = opts.get("default")
|
|
157
|
+
|
|
158
|
+
logger.info(f"Human input requested: {message[:50]}...")
|
|
159
|
+
|
|
160
|
+
# Delegate to execution context
|
|
161
|
+
response = self.execution_context.wait_for_human(
|
|
162
|
+
request_type="input",
|
|
163
|
+
message=message,
|
|
164
|
+
timeout_seconds=timeout,
|
|
165
|
+
default_value=default,
|
|
166
|
+
options=None,
|
|
167
|
+
metadata={"placeholder": placeholder},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return response.value
|
|
171
|
+
|
|
172
|
+
def review(self, options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
|
173
|
+
"""
|
|
174
|
+
Request human review (BLOCKING).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
options: Dict with:
|
|
178
|
+
- message: str - Review prompt
|
|
179
|
+
- artifact: Any - Thing to review
|
|
180
|
+
- artifact_type: str - Type of artifact
|
|
181
|
+
- options: List[str] - Available actions
|
|
182
|
+
- timeout: int - Timeout in seconds
|
|
183
|
+
- config_key: str - Reference to hitl: declaration
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dict with:
|
|
187
|
+
- decision: str - Selected option
|
|
188
|
+
- edited_artifact: Any - Modified artifact (if edited)
|
|
189
|
+
- feedback: str - Human feedback
|
|
190
|
+
|
|
191
|
+
Example (Lua):
|
|
192
|
+
local review = Human.review({
|
|
193
|
+
message = "Review this document",
|
|
194
|
+
artifact = document,
|
|
195
|
+
artifact_type = "document",
|
|
196
|
+
options = {"approve", "edit", "reject"}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if review.decision == "approve" then
|
|
200
|
+
publish(review.artifact)
|
|
201
|
+
end
|
|
202
|
+
"""
|
|
203
|
+
# Convert Lua table to dict if needed
|
|
204
|
+
opts = self._convert_lua_to_python(options) or {}
|
|
205
|
+
|
|
206
|
+
# Check for config reference
|
|
207
|
+
config_key = opts.get("config_key")
|
|
208
|
+
if config_key and config_key in self.hitl_config:
|
|
209
|
+
config_opts = self.hitl_config[config_key].copy()
|
|
210
|
+
config_opts.update(opts)
|
|
211
|
+
opts = config_opts
|
|
212
|
+
|
|
213
|
+
message = opts.get("message", "Review requested")
|
|
214
|
+
artifact = opts.get("artifact")
|
|
215
|
+
options_list = opts.get("options", ["approve", "reject"])
|
|
216
|
+
artifact_type = opts.get("artifact_type", "artifact")
|
|
217
|
+
timeout = opts.get("timeout")
|
|
218
|
+
|
|
219
|
+
logger.info(f"Human review requested: {message[:50]}...")
|
|
220
|
+
|
|
221
|
+
# Convert artifact from Lua table to Python dict
|
|
222
|
+
artifact_python = self._convert_lua_to_python(artifact) if artifact is not None else None
|
|
223
|
+
|
|
224
|
+
# Convert options list to format expected by protocol: [{label, type}, ...]
|
|
225
|
+
formatted_options = []
|
|
226
|
+
for opt in options_list:
|
|
227
|
+
# If already a dict with label/type, use as-is
|
|
228
|
+
if isinstance(opt, dict) and "label" in opt:
|
|
229
|
+
formatted_options.append(opt)
|
|
230
|
+
# Otherwise treat as string label, default to "action" type
|
|
231
|
+
else:
|
|
232
|
+
formatted_options.append({"label": str(opt).title(), "type": "action"})
|
|
233
|
+
|
|
234
|
+
# Delegate to execution context
|
|
235
|
+
response = self.execution_context.wait_for_human(
|
|
236
|
+
request_type="review",
|
|
237
|
+
message=message,
|
|
238
|
+
timeout_seconds=timeout,
|
|
239
|
+
default_value={
|
|
240
|
+
"decision": "reject",
|
|
241
|
+
"edited_artifact": artifact_python,
|
|
242
|
+
"feedback": "",
|
|
243
|
+
},
|
|
244
|
+
options=formatted_options,
|
|
245
|
+
metadata={"artifact": artifact_python, "artifact_type": artifact_type},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return response.value
|
|
249
|
+
|
|
250
|
+
def notify(self, options: Optional[Dict[str, Any]] = None) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Send notification to human (NON-BLOCKING).
|
|
253
|
+
|
|
254
|
+
Note: In Tactus core, notifications are logged but not sent to HITL handler
|
|
255
|
+
(since they're non-blocking). Implementations that need actual notification
|
|
256
|
+
delivery should use a custom notification system.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
options: Dict with:
|
|
260
|
+
- message: str - Notification message (required)
|
|
261
|
+
- level: str - info, warning, error (default: info)
|
|
262
|
+
|
|
263
|
+
Example (Lua):
|
|
264
|
+
Human.notify({
|
|
265
|
+
message = "Processing complete",
|
|
266
|
+
level = "info"
|
|
267
|
+
})
|
|
268
|
+
"""
|
|
269
|
+
# Convert Lua table to dict if needed
|
|
270
|
+
opts = self._convert_lua_to_python(options) or {}
|
|
271
|
+
|
|
272
|
+
message = opts.get("message", "Notification")
|
|
273
|
+
level = opts.get("level", "info")
|
|
274
|
+
|
|
275
|
+
logger.info(f"Human notification: [{level}] {message}")
|
|
276
|
+
|
|
277
|
+
# In base Tactus, notifications are just logged
|
|
278
|
+
# Implementations can override this to send actual notifications
|
|
279
|
+
|
|
280
|
+
def escalate(self, options: Optional[Dict[str, Any]] = None) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Escalate to human (BLOCKING).
|
|
283
|
+
|
|
284
|
+
Stops workflow execution until human resolves the issue.
|
|
285
|
+
Unlike approve/input/review, escalate has NO timeout - it blocks
|
|
286
|
+
indefinitely until a human manually resumes the procedure.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
options: Dict with:
|
|
290
|
+
- message: str - Escalation message
|
|
291
|
+
- context: Dict - Error context
|
|
292
|
+
- severity: str - Severity level (info/warning/error/critical)
|
|
293
|
+
- config_key: str - Reference to hitl: declaration
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
None - Execution resumes when human resolves
|
|
297
|
+
|
|
298
|
+
Example (Lua):
|
|
299
|
+
if attempts > 3 then
|
|
300
|
+
Human.escalate({
|
|
301
|
+
message = "Cannot resolve automatically",
|
|
302
|
+
context = {attempts = attempts, error = last_error},
|
|
303
|
+
severity = "error"
|
|
304
|
+
})
|
|
305
|
+
-- Workflow continues here after human resolves
|
|
306
|
+
end
|
|
307
|
+
"""
|
|
308
|
+
# Convert Lua tables to Python dicts recursively
|
|
309
|
+
opts = self._convert_lua_to_python(options) or {}
|
|
310
|
+
|
|
311
|
+
# Check for config reference
|
|
312
|
+
config_key = opts.get("config_key")
|
|
313
|
+
if config_key and config_key in self.hitl_config:
|
|
314
|
+
# Merge config with runtime options (runtime wins)
|
|
315
|
+
config_opts = self.hitl_config[config_key].copy()
|
|
316
|
+
config_opts.update(opts)
|
|
317
|
+
opts = config_opts
|
|
318
|
+
|
|
319
|
+
message = opts.get("message", "Escalation required")
|
|
320
|
+
context = opts.get("context", {})
|
|
321
|
+
severity = opts.get("severity", "error")
|
|
322
|
+
|
|
323
|
+
logger.warning(f"Human escalation: {message[:50]}... (severity: {severity})")
|
|
324
|
+
|
|
325
|
+
# Prepare metadata with severity and context
|
|
326
|
+
metadata = {"severity": severity, "context": context}
|
|
327
|
+
|
|
328
|
+
# Delegate to execution context
|
|
329
|
+
# No timeout, no default - blocks until human resolves
|
|
330
|
+
self.execution_context.wait_for_human(
|
|
331
|
+
request_type="escalation",
|
|
332
|
+
message=message,
|
|
333
|
+
timeout_seconds=None, # No timeout - wait indefinitely
|
|
334
|
+
default_value=None, # No default - human must resolve
|
|
335
|
+
options=None,
|
|
336
|
+
metadata=metadata,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
logger.info("Human escalation resolved - resuming workflow")
|
|
340
|
+
|
|
341
|
+
def __repr__(self) -> str:
|
|
342
|
+
return f"HumanPrimitive(config_keys={list(self.hitl_config.keys())})"
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Json Primitive - JSON serialization/deserialization.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Json.encode(data) - Serialize Lua table/value to JSON string
|
|
6
|
+
- Json.decode(json_str) - Parse JSON string to Lua table
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JsonPrimitive:
|
|
17
|
+
"""
|
|
18
|
+
Handles JSON encoding and decoding for procedures.
|
|
19
|
+
|
|
20
|
+
Enables workflows to:
|
|
21
|
+
- Serialize Lua tables to JSON strings
|
|
22
|
+
- Parse JSON strings into Lua tables
|
|
23
|
+
- Handle data interchange with external systems
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, lua_sandbox=None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize Json primitive.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
lua_sandbox: LuaSandbox for creating Lua tables (optional)
|
|
32
|
+
"""
|
|
33
|
+
self.lua_sandbox = lua_sandbox
|
|
34
|
+
logger.debug("JsonPrimitive initialized")
|
|
35
|
+
|
|
36
|
+
def encode(self, data: Any) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Encode Lua data structure to JSON string.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: Lua table or primitive value to encode
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
JSON string representation
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If data cannot be serialized to JSON
|
|
48
|
+
|
|
49
|
+
Example (Lua):
|
|
50
|
+
local user = {
|
|
51
|
+
name = "Alice",
|
|
52
|
+
age = 30,
|
|
53
|
+
active = true
|
|
54
|
+
}
|
|
55
|
+
local json_str = Json.encode(user)
|
|
56
|
+
Log.info("Encoded JSON", {json = json_str})
|
|
57
|
+
-- json_str = '{"name": "Alice", "age": 30, "active": true}'
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# Convert Lua tables to Python dicts recursively if needed
|
|
61
|
+
python_data = self._lua_to_python(data)
|
|
62
|
+
|
|
63
|
+
json_str = json.dumps(python_data, ensure_ascii=False, indent=None)
|
|
64
|
+
logger.debug(f"Encoded data to JSON ({len(json_str)} bytes)")
|
|
65
|
+
return json_str
|
|
66
|
+
|
|
67
|
+
except (TypeError, ValueError) as e:
|
|
68
|
+
error_msg = f"Failed to encode to JSON: {e}"
|
|
69
|
+
logger.error(error_msg)
|
|
70
|
+
raise ValueError(error_msg)
|
|
71
|
+
|
|
72
|
+
def decode(self, json_str: str):
|
|
73
|
+
"""
|
|
74
|
+
Decode JSON string to Lua table.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
json_str: JSON string to parse
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Lua table with parsed data
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If JSON is malformed
|
|
84
|
+
|
|
85
|
+
Example (Lua):
|
|
86
|
+
local json_str = '{"name": "Bob", "scores": [85, 92, 78]}'
|
|
87
|
+
local data = Json.decode(json_str)
|
|
88
|
+
Log.info("Decoded data", {
|
|
89
|
+
name = data.name,
|
|
90
|
+
first_score = data.scores[1]
|
|
91
|
+
})
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
# Parse JSON to Python dict
|
|
95
|
+
python_data = json.loads(json_str)
|
|
96
|
+
logger.debug(f"Decoded JSON string ({len(json_str)} bytes)")
|
|
97
|
+
|
|
98
|
+
# Convert to Lua table if lua_sandbox available
|
|
99
|
+
if self.lua_sandbox:
|
|
100
|
+
return self._python_to_lua(python_data)
|
|
101
|
+
else:
|
|
102
|
+
# Fallback: return Python dict (will work but not ideal)
|
|
103
|
+
return python_data
|
|
104
|
+
|
|
105
|
+
except json.JSONDecodeError as e:
|
|
106
|
+
error_msg = f"Failed to decode JSON: {e}"
|
|
107
|
+
logger.error(error_msg)
|
|
108
|
+
raise ValueError(error_msg)
|
|
109
|
+
|
|
110
|
+
def _lua_to_python(self, value: Any) -> Any:
|
|
111
|
+
"""
|
|
112
|
+
Recursively convert Lua tables to Python dicts/lists.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
value: Lua value to convert
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Python equivalent (dict, list, or primitive)
|
|
119
|
+
"""
|
|
120
|
+
# Import lupa for table checking
|
|
121
|
+
try:
|
|
122
|
+
from lupa import lua_type
|
|
123
|
+
|
|
124
|
+
# Check if it's a Lua table
|
|
125
|
+
if lua_type(value) == "table":
|
|
126
|
+
# Try to determine if it's an array or dict
|
|
127
|
+
# Lua arrays have consecutive integer keys starting at 1
|
|
128
|
+
result = {}
|
|
129
|
+
is_array = True
|
|
130
|
+
keys = []
|
|
131
|
+
|
|
132
|
+
for k, v in value.items():
|
|
133
|
+
keys.append(k)
|
|
134
|
+
result[k] = self._lua_to_python(v)
|
|
135
|
+
if not isinstance(k, int) or k < 1:
|
|
136
|
+
is_array = False
|
|
137
|
+
|
|
138
|
+
# Check if keys are consecutive integers starting at 1
|
|
139
|
+
if is_array and keys:
|
|
140
|
+
keys_sorted = sorted(keys)
|
|
141
|
+
if keys_sorted != list(range(1, len(keys) + 1)):
|
|
142
|
+
is_array = False
|
|
143
|
+
|
|
144
|
+
# Convert to list if it's an array
|
|
145
|
+
if is_array and keys:
|
|
146
|
+
return [result[i] for i in range(1, len(keys) + 1)]
|
|
147
|
+
else:
|
|
148
|
+
return result
|
|
149
|
+
else:
|
|
150
|
+
# Primitive value
|
|
151
|
+
return value
|
|
152
|
+
|
|
153
|
+
except ImportError:
|
|
154
|
+
# If lupa not available, just return as-is
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
def _python_to_lua(self, value: Any):
|
|
158
|
+
"""
|
|
159
|
+
Recursively convert Python dicts/lists to Lua tables.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
value: Python value to convert
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Lua table or primitive value
|
|
166
|
+
"""
|
|
167
|
+
if not self.lua_sandbox:
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
if isinstance(value, dict):
|
|
171
|
+
# Convert dict to Lua table
|
|
172
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
173
|
+
for k, v in value.items():
|
|
174
|
+
lua_table[k] = self._python_to_lua(v)
|
|
175
|
+
return lua_table
|
|
176
|
+
|
|
177
|
+
elif isinstance(value, (list, tuple)):
|
|
178
|
+
# Convert list to Lua array (1-indexed)
|
|
179
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
180
|
+
for i, item in enumerate(value, start=1):
|
|
181
|
+
lua_table[i] = self._python_to_lua(item)
|
|
182
|
+
return lua_table
|
|
183
|
+
|
|
184
|
+
else:
|
|
185
|
+
# Primitive value (str, int, float, bool, None)
|
|
186
|
+
return value
|
|
187
|
+
|
|
188
|
+
def __repr__(self) -> str:
|
|
189
|
+
return "JsonPrimitive()"
|