tactus 0.34.1__py3-none-any.whl → 0.35.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 +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +15 -6
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
tactus/primitives/human.py
CHANGED
|
@@ -16,7 +16,7 @@ Deprecated:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import logging
|
|
19
|
-
from typing import Any,
|
|
19
|
+
from typing import Any, Optional
|
|
20
20
|
|
|
21
21
|
logger = logging.getLogger(__name__)
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ class HumanPrimitive:
|
|
|
29
29
|
actual human interactions (via CLI, web UI, API, etc.).
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
def __init__(self, execution_context, hitl_config: Optional[
|
|
32
|
+
def __init__(self, execution_context, hitl_config: Optional[dict[str, Any]] = None):
|
|
33
33
|
"""
|
|
34
34
|
Initialize Human primitive.
|
|
35
35
|
|
|
@@ -41,39 +41,41 @@ class HumanPrimitive:
|
|
|
41
41
|
self.hitl_config = hitl_config or {}
|
|
42
42
|
logger.debug("HumanPrimitive initialized")
|
|
43
43
|
|
|
44
|
-
def _convert_lua_to_python(self,
|
|
44
|
+
def _convert_lua_to_python(self, lua_value: Any) -> Any:
|
|
45
45
|
"""Recursively convert Lua tables to Python dicts or lists."""
|
|
46
|
-
if
|
|
46
|
+
if lua_value is None:
|
|
47
47
|
return None
|
|
48
48
|
# Check if it's a Lua table (has .items() but not a dict)
|
|
49
|
-
if hasattr(
|
|
49
|
+
if hasattr(lua_value, "items") and not isinstance(lua_value, dict):
|
|
50
50
|
# Get all items from the Lua table
|
|
51
|
-
|
|
51
|
+
table_items = list(lua_value.items())
|
|
52
52
|
|
|
53
53
|
# Check if this is an array-like table (numeric keys starting from 1)
|
|
54
|
-
if
|
|
54
|
+
if table_items and all(isinstance(key, int) for key, _ in table_items):
|
|
55
55
|
# Sort by key and extract values to create a Python list
|
|
56
|
-
sorted_items = sorted(
|
|
56
|
+
sorted_items = sorted(table_items, key=lambda item: item[0])
|
|
57
57
|
# Check if keys are consecutive starting from 1
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
sorted_keys = [key for key, _ in sorted_items]
|
|
59
|
+
expected_keys = list(range(1, len(sorted_items) + 1))
|
|
60
|
+
if sorted_keys == expected_keys:
|
|
61
|
+
return [self._convert_lua_to_python(value) for _, value in sorted_items]
|
|
60
62
|
|
|
61
63
|
# Otherwise, convert to dict (string keys or mixed)
|
|
62
64
|
result = {}
|
|
63
|
-
for key, value in
|
|
65
|
+
for key, value in table_items:
|
|
64
66
|
result[key] = self._convert_lua_to_python(value)
|
|
65
67
|
return result
|
|
66
|
-
elif isinstance(
|
|
68
|
+
elif isinstance(lua_value, dict):
|
|
67
69
|
# Recursively convert nested dicts
|
|
68
|
-
return {
|
|
69
|
-
elif isinstance(
|
|
70
|
+
return {key: self._convert_lua_to_python(value) for key, value in lua_value.items()}
|
|
71
|
+
elif isinstance(lua_value, (list, tuple)):
|
|
70
72
|
# Recursively convert lists
|
|
71
|
-
return [self._convert_lua_to_python(item) for item in
|
|
73
|
+
return [self._convert_lua_to_python(item) for item in lua_value]
|
|
72
74
|
else:
|
|
73
75
|
# Primitive type, return as-is
|
|
74
|
-
return
|
|
76
|
+
return lua_value
|
|
75
77
|
|
|
76
|
-
def approve(self, options: Optional[
|
|
78
|
+
def approve(self, options: Optional[dict[str, Any]] = None) -> bool:
|
|
77
79
|
"""
|
|
78
80
|
Request yes/no approval from human (BLOCKING).
|
|
79
81
|
|
|
@@ -105,26 +107,26 @@ class HumanPrimitive:
|
|
|
105
107
|
end
|
|
106
108
|
"""
|
|
107
109
|
# Convert Lua tables to Python dicts recursively
|
|
108
|
-
|
|
110
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
109
111
|
|
|
110
112
|
# Support string message shorthand: Human.approve("message")
|
|
111
|
-
if isinstance(
|
|
112
|
-
|
|
113
|
+
if isinstance(options_dict, str):
|
|
114
|
+
options_dict = {"message": options_dict}
|
|
113
115
|
|
|
114
116
|
# Check for config reference
|
|
115
|
-
config_key =
|
|
117
|
+
config_key = options_dict.get("config_key")
|
|
116
118
|
if config_key and config_key in self.hitl_config:
|
|
117
119
|
# Merge config with runtime options (runtime wins)
|
|
118
120
|
config_opts = self.hitl_config[config_key].copy()
|
|
119
|
-
config_opts.update(
|
|
120
|
-
|
|
121
|
+
config_opts.update(options_dict)
|
|
122
|
+
options_dict = config_opts
|
|
121
123
|
|
|
122
|
-
message =
|
|
123
|
-
context =
|
|
124
|
-
timeout =
|
|
125
|
-
default =
|
|
124
|
+
message = options_dict.get("message", "Approval requested")
|
|
125
|
+
context = options_dict.get("context", {})
|
|
126
|
+
timeout = options_dict.get("timeout")
|
|
127
|
+
default = options_dict.get("default", False)
|
|
126
128
|
|
|
127
|
-
logger.info(
|
|
129
|
+
logger.info("Human approval requested: %s...", message[:50])
|
|
128
130
|
|
|
129
131
|
# CRITICAL: Wrap HITL call in checkpoint for transparent durability
|
|
130
132
|
# This allows kill/resume to work - procedure can be restarted and will resume from this point
|
|
@@ -141,16 +143,19 @@ class HumanPrimitive:
|
|
|
141
143
|
)
|
|
142
144
|
|
|
143
145
|
response = self.execution_context.checkpoint(checkpoint_fn, "hitl_approval")
|
|
144
|
-
logger.debug(
|
|
146
|
+
logger.debug(
|
|
147
|
+
"[CHECKPOINT] Human.approve() checkpoint completed, response=%s",
|
|
148
|
+
response,
|
|
149
|
+
)
|
|
145
150
|
|
|
146
151
|
return response.value
|
|
147
152
|
|
|
148
|
-
def input(self, options: Optional[
|
|
153
|
+
def input(self, options: Optional[dict[str, Any]] = None) -> Optional[str]:
|
|
149
154
|
"""
|
|
150
155
|
Request free-form input from human (BLOCKING).
|
|
151
156
|
|
|
152
157
|
Args:
|
|
153
|
-
options:
|
|
158
|
+
options: dict with:
|
|
154
159
|
- message: str - Prompt for human
|
|
155
160
|
- placeholder: str - Input placeholder
|
|
156
161
|
- timeout: int - Timeout in seconds
|
|
@@ -172,21 +177,21 @@ class HumanPrimitive:
|
|
|
172
177
|
end
|
|
173
178
|
"""
|
|
174
179
|
# Convert Lua table to dict if needed
|
|
175
|
-
|
|
180
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
176
181
|
|
|
177
182
|
# Check for config reference
|
|
178
|
-
config_key =
|
|
183
|
+
config_key = options_dict.get("config_key")
|
|
179
184
|
if config_key and config_key in self.hitl_config:
|
|
180
185
|
config_opts = self.hitl_config[config_key].copy()
|
|
181
|
-
config_opts.update(
|
|
182
|
-
|
|
186
|
+
config_opts.update(options_dict)
|
|
187
|
+
options_dict = config_opts
|
|
183
188
|
|
|
184
|
-
message =
|
|
185
|
-
placeholder =
|
|
186
|
-
timeout =
|
|
187
|
-
default =
|
|
189
|
+
message = options_dict.get("message", "Input requested")
|
|
190
|
+
placeholder = options_dict.get("placeholder", "")
|
|
191
|
+
timeout = options_dict.get("timeout")
|
|
192
|
+
default = options_dict.get("default")
|
|
188
193
|
|
|
189
|
-
logger.info(
|
|
194
|
+
logger.info("Human input requested: %s...", message[:50])
|
|
190
195
|
|
|
191
196
|
# CRITICAL: Wrap HITL call in checkpoint for transparent durability
|
|
192
197
|
def checkpoint_fn():
|
|
@@ -203,21 +208,21 @@ class HumanPrimitive:
|
|
|
203
208
|
|
|
204
209
|
return response.value
|
|
205
210
|
|
|
206
|
-
def review(self, options: Optional[
|
|
211
|
+
def review(self, options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
|
|
207
212
|
"""
|
|
208
213
|
Request human review (BLOCKING).
|
|
209
214
|
|
|
210
215
|
Args:
|
|
211
|
-
options:
|
|
216
|
+
options: dict with:
|
|
212
217
|
- message: str - Review prompt
|
|
213
218
|
- artifact: Any - Thing to review
|
|
214
219
|
- artifact_type: str - Type of artifact
|
|
215
|
-
- options:
|
|
220
|
+
- options: list[str] - Available actions
|
|
216
221
|
- timeout: int - Timeout in seconds
|
|
217
222
|
- config_key: str - Reference to hitl: declaration
|
|
218
223
|
|
|
219
224
|
Returns:
|
|
220
|
-
|
|
225
|
+
dict with:
|
|
221
226
|
- decision: str - Selected option
|
|
222
227
|
- edited_artifact: Any - Modified artifact (if edited)
|
|
223
228
|
- feedback: str - Human feedback
|
|
@@ -235,35 +240,35 @@ class HumanPrimitive:
|
|
|
235
240
|
end
|
|
236
241
|
"""
|
|
237
242
|
# Convert Lua table to dict if needed
|
|
238
|
-
|
|
243
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
239
244
|
|
|
240
245
|
# Check for config reference
|
|
241
|
-
config_key =
|
|
246
|
+
config_key = options_dict.get("config_key")
|
|
242
247
|
if config_key and config_key in self.hitl_config:
|
|
243
248
|
config_opts = self.hitl_config[config_key].copy()
|
|
244
|
-
config_opts.update(
|
|
245
|
-
|
|
249
|
+
config_opts.update(options_dict)
|
|
250
|
+
options_dict = config_opts
|
|
246
251
|
|
|
247
|
-
message =
|
|
248
|
-
artifact =
|
|
249
|
-
options_list =
|
|
250
|
-
artifact_type =
|
|
251
|
-
timeout =
|
|
252
|
+
message = options_dict.get("message", "Review requested")
|
|
253
|
+
artifact = options_dict.get("artifact")
|
|
254
|
+
options_list = options_dict.get("options", ["approve", "reject"])
|
|
255
|
+
artifact_type = options_dict.get("artifact_type", "artifact")
|
|
256
|
+
timeout = options_dict.get("timeout")
|
|
252
257
|
|
|
253
|
-
logger.info(
|
|
258
|
+
logger.info("Human review requested: %s...", message[:50])
|
|
254
259
|
|
|
255
260
|
# Convert artifact from Lua table to Python dict
|
|
256
261
|
artifact_python = self._convert_lua_to_python(artifact) if artifact is not None else None
|
|
257
262
|
|
|
258
263
|
# Convert options list to format expected by protocol: [{label, type}, ...]
|
|
259
264
|
formatted_options = []
|
|
260
|
-
for
|
|
265
|
+
for option_entry in options_list:
|
|
261
266
|
# If already a dict with label/type, use as-is
|
|
262
|
-
if isinstance(
|
|
263
|
-
formatted_options.append(
|
|
267
|
+
if isinstance(option_entry, dict) and "label" in option_entry:
|
|
268
|
+
formatted_options.append(option_entry)
|
|
264
269
|
# Otherwise treat as string label, default to "action" type
|
|
265
270
|
else:
|
|
266
|
-
formatted_options.append({"label": str(
|
|
271
|
+
formatted_options.append({"label": str(option_entry).title(), "type": "action"})
|
|
267
272
|
|
|
268
273
|
# CRITICAL: Wrap HITL call in checkpoint for transparent durability
|
|
269
274
|
def checkpoint_fn():
|
|
@@ -284,7 +289,7 @@ class HumanPrimitive:
|
|
|
284
289
|
|
|
285
290
|
return response.value
|
|
286
291
|
|
|
287
|
-
def notify(self, options: Optional[
|
|
292
|
+
def notify(self, options: Optional[dict[str, Any]] = None) -> None:
|
|
288
293
|
"""
|
|
289
294
|
Send notification to human (NON-BLOCKING).
|
|
290
295
|
|
|
@@ -293,7 +298,7 @@ class HumanPrimitive:
|
|
|
293
298
|
delivery should use a custom notification system.
|
|
294
299
|
|
|
295
300
|
Args:
|
|
296
|
-
options:
|
|
301
|
+
options: dict with:
|
|
297
302
|
- message: str - Notification message (required)
|
|
298
303
|
- level: str - info, warning, error (default: info)
|
|
299
304
|
|
|
@@ -304,17 +309,17 @@ class HumanPrimitive:
|
|
|
304
309
|
})
|
|
305
310
|
"""
|
|
306
311
|
# Convert Lua table to dict if needed
|
|
307
|
-
|
|
312
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
308
313
|
|
|
309
|
-
message =
|
|
310
|
-
level =
|
|
314
|
+
message = options_dict.get("message", "Notification")
|
|
315
|
+
level = options_dict.get("level", "info")
|
|
311
316
|
|
|
312
|
-
logger.info(
|
|
317
|
+
logger.info("Human notification: [%s] %s", level, message)
|
|
313
318
|
|
|
314
319
|
# In base Tactus, notifications are just logged
|
|
315
320
|
# Implementations can override this to send actual notifications
|
|
316
321
|
|
|
317
|
-
def escalate(self, options: Optional[
|
|
322
|
+
def escalate(self, options: Optional[dict[str, Any]] = None) -> None:
|
|
318
323
|
"""
|
|
319
324
|
Escalate to human (BLOCKING).
|
|
320
325
|
|
|
@@ -323,7 +328,7 @@ class HumanPrimitive:
|
|
|
323
328
|
indefinitely until a human manually resumes the procedure.
|
|
324
329
|
|
|
325
330
|
Args:
|
|
326
|
-
options:
|
|
331
|
+
options: dict with:
|
|
327
332
|
- message: str - Escalation message
|
|
328
333
|
- context: Dict - Error context
|
|
329
334
|
- severity: str - Severity level (info/warning/error/critical)
|
|
@@ -343,21 +348,21 @@ class HumanPrimitive:
|
|
|
343
348
|
end
|
|
344
349
|
"""
|
|
345
350
|
# Convert Lua tables to Python dicts recursively
|
|
346
|
-
|
|
351
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
347
352
|
|
|
348
353
|
# Check for config reference
|
|
349
|
-
config_key =
|
|
354
|
+
config_key = options_dict.get("config_key")
|
|
350
355
|
if config_key and config_key in self.hitl_config:
|
|
351
356
|
# Merge config with runtime options (runtime wins)
|
|
352
357
|
config_opts = self.hitl_config[config_key].copy()
|
|
353
|
-
config_opts.update(
|
|
354
|
-
|
|
358
|
+
config_opts.update(options_dict)
|
|
359
|
+
options_dict = config_opts
|
|
355
360
|
|
|
356
|
-
message =
|
|
357
|
-
context =
|
|
358
|
-
severity =
|
|
361
|
+
message = options_dict.get("message", "Escalation required")
|
|
362
|
+
context = options_dict.get("context", {})
|
|
363
|
+
severity = options_dict.get("severity", "error")
|
|
359
364
|
|
|
360
|
-
logger.warning(
|
|
365
|
+
logger.warning("Human escalation: %s... (severity: %s)", message[:50], severity)
|
|
361
366
|
|
|
362
367
|
# Prepare metadata with severity and context
|
|
363
368
|
metadata = {"severity": severity, "context": context}
|
|
@@ -377,7 +382,7 @@ class HumanPrimitive:
|
|
|
377
382
|
|
|
378
383
|
logger.info("Human escalation resolved - resuming workflow")
|
|
379
384
|
|
|
380
|
-
def select(self, options: Optional[
|
|
385
|
+
def select(self, options: Optional[dict[str, Any]] = None) -> Any:
|
|
381
386
|
"""
|
|
382
387
|
Request selection from options (BLOCKING).
|
|
383
388
|
|
|
@@ -386,7 +391,7 @@ class HumanPrimitive:
|
|
|
386
391
|
Args:
|
|
387
392
|
options: Dict with:
|
|
388
393
|
- message: str - Prompt for human
|
|
389
|
-
- options:
|
|
394
|
+
- options: list[str] or list[dict] - Available choices
|
|
390
395
|
- mode: str - "single" (default) or "multiple"
|
|
391
396
|
- style: str - UI hint: "radio", "dropdown", "checkbox" (optional)
|
|
392
397
|
- min: int - Minimum selections required (for multiple mode)
|
|
@@ -397,7 +402,7 @@ class HumanPrimitive:
|
|
|
397
402
|
|
|
398
403
|
Returns:
|
|
399
404
|
For single mode: str - Selected option value
|
|
400
|
-
For multiple mode:
|
|
405
|
+
For multiple mode: list[str] - Selected option values
|
|
401
406
|
|
|
402
407
|
Example (Lua):
|
|
403
408
|
-- Single select (radio buttons)
|
|
@@ -418,38 +423,40 @@ class HumanPrimitive:
|
|
|
418
423
|
})
|
|
419
424
|
"""
|
|
420
425
|
# Convert Lua tables to Python dicts recursively
|
|
421
|
-
|
|
426
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
422
427
|
|
|
423
428
|
# Check for config reference
|
|
424
|
-
config_key =
|
|
429
|
+
config_key = options_dict.get("config_key")
|
|
425
430
|
if config_key and config_key in self.hitl_config:
|
|
426
431
|
config_opts = self.hitl_config[config_key].copy()
|
|
427
|
-
config_opts.update(
|
|
428
|
-
|
|
432
|
+
config_opts.update(options_dict)
|
|
433
|
+
options_dict = config_opts
|
|
429
434
|
|
|
430
|
-
message =
|
|
431
|
-
options_list =
|
|
432
|
-
mode =
|
|
433
|
-
style =
|
|
434
|
-
min_selections =
|
|
435
|
-
max_selections =
|
|
436
|
-
timeout =
|
|
437
|
-
default =
|
|
435
|
+
message = options_dict.get("message", "Selection required")
|
|
436
|
+
options_list = options_dict.get("options", [])
|
|
437
|
+
mode = options_dict.get("mode", "single")
|
|
438
|
+
style = options_dict.get("style") # UI hint: radio, dropdown, checkbox
|
|
439
|
+
min_selections = options_dict.get("min", 1 if mode == "multiple" else None)
|
|
440
|
+
max_selections = options_dict.get("max")
|
|
441
|
+
timeout = options_dict.get("timeout")
|
|
442
|
+
default = options_dict.get("default", [] if mode == "multiple" else None)
|
|
438
443
|
|
|
439
|
-
logger.info(
|
|
444
|
+
logger.info("Human selection requested (%s): %s...", mode, message[:50])
|
|
440
445
|
|
|
441
446
|
# Convert options list to format expected by protocol: [{label, value}, ...]
|
|
442
447
|
formatted_options = []
|
|
443
|
-
for
|
|
444
|
-
if isinstance(
|
|
448
|
+
for option_entry in options_list:
|
|
449
|
+
if isinstance(option_entry, dict) and "label" in option_entry:
|
|
445
450
|
# Already formatted: {label: "...", value: "..."}
|
|
446
|
-
formatted_options.append(
|
|
447
|
-
elif isinstance(
|
|
451
|
+
formatted_options.append(option_entry)
|
|
452
|
+
elif isinstance(option_entry, dict) and "value" in option_entry:
|
|
448
453
|
# Has value but no label - use value as label
|
|
449
|
-
formatted_options.append(
|
|
454
|
+
formatted_options.append(
|
|
455
|
+
{"label": str(option_entry["value"]), "value": option_entry["value"]}
|
|
456
|
+
)
|
|
450
457
|
else:
|
|
451
458
|
# Simple string - use as both label and value
|
|
452
|
-
formatted_options.append({"label": str(
|
|
459
|
+
formatted_options.append({"label": str(option_entry), "value": option_entry})
|
|
453
460
|
|
|
454
461
|
# Build metadata with select-specific fields
|
|
455
462
|
metadata = {
|
|
@@ -475,7 +482,7 @@ class HumanPrimitive:
|
|
|
475
482
|
|
|
476
483
|
return response.value
|
|
477
484
|
|
|
478
|
-
def upload(self, options: Optional[
|
|
485
|
+
def upload(self, options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
|
|
479
486
|
"""
|
|
480
487
|
Request file upload from human (BLOCKING).
|
|
481
488
|
|
|
@@ -485,14 +492,14 @@ class HumanPrimitive:
|
|
|
485
492
|
Args:
|
|
486
493
|
options: Dict with:
|
|
487
494
|
- message: str - Upload prompt
|
|
488
|
-
- accept: str or
|
|
495
|
+
- accept: str or list[str] - Accepted file types (e.g., ".pdf,.doc" or ["image/*"])
|
|
489
496
|
- max_size: str or int - Maximum file size (e.g., "10MB" or 10485760)
|
|
490
497
|
- multiple: bool - Allow multiple files (default: False)
|
|
491
498
|
- timeout: int - Timeout in seconds
|
|
492
499
|
- config_key: str - Reference to hitl: declaration
|
|
493
500
|
|
|
494
501
|
Returns:
|
|
495
|
-
|
|
502
|
+
dict with file info (or list[dict] if multiple=True):
|
|
496
503
|
- path: str - Local filesystem path to uploaded file
|
|
497
504
|
- name: str - Original filename
|
|
498
505
|
- size: int - File size in bytes
|
|
@@ -527,22 +534,22 @@ class HumanPrimitive:
|
|
|
527
534
|
end
|
|
528
535
|
"""
|
|
529
536
|
# Convert Lua tables to Python dicts recursively
|
|
530
|
-
|
|
537
|
+
options_dict = self._convert_lua_to_python(options) or {}
|
|
531
538
|
|
|
532
539
|
# Check for config reference
|
|
533
|
-
config_key =
|
|
540
|
+
config_key = options_dict.get("config_key")
|
|
534
541
|
if config_key and config_key in self.hitl_config:
|
|
535
542
|
config_opts = self.hitl_config[config_key].copy()
|
|
536
|
-
config_opts.update(
|
|
537
|
-
|
|
543
|
+
config_opts.update(options_dict)
|
|
544
|
+
options_dict = config_opts
|
|
538
545
|
|
|
539
|
-
message =
|
|
540
|
-
accept =
|
|
541
|
-
max_size =
|
|
542
|
-
multiple =
|
|
543
|
-
timeout =
|
|
546
|
+
message = options_dict.get("message", "File upload requested")
|
|
547
|
+
accept = options_dict.get("accept") # File type filter
|
|
548
|
+
max_size = options_dict.get("max_size") # Size limit
|
|
549
|
+
multiple = options_dict.get("multiple", False)
|
|
550
|
+
timeout = options_dict.get("timeout")
|
|
544
551
|
|
|
545
|
-
logger.info(
|
|
552
|
+
logger.info("Human file upload requested: %s...", message[:50])
|
|
546
553
|
|
|
547
554
|
# Normalize accept to list
|
|
548
555
|
if isinstance(accept, str):
|
|
@@ -593,10 +600,10 @@ class HumanPrimitive:
|
|
|
593
600
|
try:
|
|
594
601
|
return int(size_str)
|
|
595
602
|
except ValueError:
|
|
596
|
-
logger.warning(
|
|
603
|
+
logger.warning("Could not parse size '%s', using default", size_str)
|
|
597
604
|
return 10 * 1024 * 1024 # Default 10MB
|
|
598
605
|
|
|
599
|
-
def inputs(self, items: Optional[
|
|
606
|
+
def inputs(self, items: Optional[list[dict[str, Any]]] = None) -> dict[str, Any]:
|
|
600
607
|
"""
|
|
601
608
|
Request multiple inputs from human in a single interaction (BLOCKING).
|
|
602
609
|
|
|
@@ -607,19 +614,19 @@ class HumanPrimitive:
|
|
|
607
614
|
before submitting a single response.
|
|
608
615
|
|
|
609
616
|
Args:
|
|
610
|
-
items:
|
|
617
|
+
items: list of input items, each with:
|
|
611
618
|
- id: str - Unique ID for this item (required)
|
|
612
619
|
- label: str - Short label for tabs (required)
|
|
613
620
|
- type: str - Request type: "approval", "input", "select", etc. (required)
|
|
614
621
|
- message: str - Prompt for this input (required)
|
|
615
|
-
- options:
|
|
622
|
+
- options: list - Options for select/review types
|
|
616
623
|
- required: bool - Whether this input is required (default: True)
|
|
617
|
-
- metadata:
|
|
624
|
+
- metadata: dict - Type-specific metadata
|
|
618
625
|
- timeout: int - Timeout in seconds
|
|
619
626
|
- default: Any - Default value
|
|
620
627
|
|
|
621
628
|
Returns:
|
|
622
|
-
|
|
629
|
+
dict keyed by item ID with response values:
|
|
623
630
|
{
|
|
624
631
|
"target": "production",
|
|
625
632
|
"confirm": True,
|
|
@@ -661,10 +668,12 @@ class HumanPrimitive:
|
|
|
661
668
|
)
|
|
662
669
|
|
|
663
670
|
# Convert Lua tables to Python dicts recursively
|
|
664
|
-
logger.debug(
|
|
671
|
+
logger.debug("Human.inputs() called with items type: %s", type(items))
|
|
665
672
|
items_list = self._convert_lua_to_python(items) or []
|
|
666
673
|
logger.debug(
|
|
667
|
-
|
|
674
|
+
"Converted to items_list, length: %s, type: %s",
|
|
675
|
+
len(items_list),
|
|
676
|
+
type(items_list),
|
|
668
677
|
)
|
|
669
678
|
|
|
670
679
|
if not items_list:
|
|
@@ -672,15 +681,18 @@ class HumanPrimitive:
|
|
|
672
681
|
|
|
673
682
|
# Validate items
|
|
674
683
|
seen_ids = set()
|
|
675
|
-
for
|
|
684
|
+
for index, item in enumerate(items_list):
|
|
676
685
|
logger.debug(
|
|
677
|
-
|
|
686
|
+
"Validating item %s: type=%s, keys=%s",
|
|
687
|
+
index,
|
|
688
|
+
type(item),
|
|
689
|
+
list(item.keys()) if isinstance(item, dict) else "NOT A DICT",
|
|
678
690
|
)
|
|
679
691
|
|
|
680
692
|
# Ensure item is a dict
|
|
681
693
|
if not isinstance(item, dict):
|
|
682
694
|
raise ValueError(
|
|
683
|
-
f"Item {
|
|
695
|
+
f"Item {index} is not a dictionary (got {type(item).__name__}): {item}"
|
|
684
696
|
)
|
|
685
697
|
|
|
686
698
|
# Validate required fields
|
|
@@ -699,7 +711,7 @@ class HumanPrimitive:
|
|
|
699
711
|
raise ValueError(f"Duplicate item ID: {item_id}")
|
|
700
712
|
seen_ids.add(item_id)
|
|
701
713
|
|
|
702
|
-
logger.info(
|
|
714
|
+
logger.info("Human inputs requested: %s items", len(items_list))
|
|
703
715
|
|
|
704
716
|
# Build ControlRequestItem list
|
|
705
717
|
from tactus.protocols.control import ControlRequestItem
|
|
@@ -709,13 +721,18 @@ class HumanPrimitive:
|
|
|
709
721
|
# Convert options if present
|
|
710
722
|
options_list = item.get("options", [])
|
|
711
723
|
formatted_options = []
|
|
712
|
-
for
|
|
713
|
-
if isinstance(
|
|
714
|
-
formatted_options.append(
|
|
715
|
-
elif isinstance(
|
|
716
|
-
formatted_options.append(
|
|
724
|
+
for option_entry in options_list:
|
|
725
|
+
if isinstance(option_entry, dict) and "label" in option_entry:
|
|
726
|
+
formatted_options.append(option_entry)
|
|
727
|
+
elif isinstance(option_entry, dict) and "value" in option_entry:
|
|
728
|
+
formatted_options.append(
|
|
729
|
+
{
|
|
730
|
+
"label": str(option_entry["value"]),
|
|
731
|
+
"value": option_entry["value"],
|
|
732
|
+
}
|
|
733
|
+
)
|
|
717
734
|
else:
|
|
718
|
-
formatted_options.append({"label": str(
|
|
735
|
+
formatted_options.append({"label": str(option_entry), "value": option_entry})
|
|
719
736
|
|
|
720
737
|
# Build metadata
|
|
721
738
|
metadata = item.get("metadata", {})
|
|
@@ -762,7 +779,7 @@ class HumanPrimitive:
|
|
|
762
779
|
|
|
763
780
|
return lua_runtime.table_from(converted_result)
|
|
764
781
|
|
|
765
|
-
def multiple(self, items: Optional[
|
|
782
|
+
def multiple(self, items: Optional[list[dict[str, Any]]] = None) -> dict[str, Any]:
|
|
766
783
|
"""
|
|
767
784
|
Request multiple inputs from human in a single interaction (BLOCKING).
|
|
768
785
|
|
|
@@ -773,19 +790,19 @@ class HumanPrimitive:
|
|
|
773
790
|
them all before submitting a single response.
|
|
774
791
|
|
|
775
792
|
Args:
|
|
776
|
-
items:
|
|
793
|
+
items: list of input items, each with:
|
|
777
794
|
- id: str - Unique ID for this item (required)
|
|
778
795
|
- label: str - Short label for tabs (required)
|
|
779
796
|
- type: str - Request type: "approval", "input", "select", etc. (required)
|
|
780
797
|
- message: str - Prompt for this input (required)
|
|
781
|
-
- options:
|
|
798
|
+
- options: list - Options for select/review types
|
|
782
799
|
- required: bool - Whether this input is required (default: True)
|
|
783
|
-
- metadata:
|
|
800
|
+
- metadata: dict - Type-specific metadata
|
|
784
801
|
- timeout: int - Timeout in seconds
|
|
785
802
|
- default: Any - Default value
|
|
786
803
|
|
|
787
804
|
Returns:
|
|
788
|
-
|
|
805
|
+
dict keyed by item ID with response values:
|
|
789
806
|
{
|
|
790
807
|
"target": "production",
|
|
791
808
|
"confirm": True,
|
|
@@ -822,7 +839,7 @@ class HumanPrimitive:
|
|
|
822
839
|
"""
|
|
823
840
|
return self.inputs(items)
|
|
824
841
|
|
|
825
|
-
def custom(self, options: Optional[
|
|
842
|
+
def custom(self, options: Optional[dict[str, Any]] = None) -> Any:
|
|
826
843
|
"""
|
|
827
844
|
Request custom component interaction from human (BLOCKING).
|
|
828
845
|
|
|
@@ -830,11 +847,11 @@ class HumanPrimitive:
|
|
|
830
847
|
The component receives all metadata and can return arbitrary values.
|
|
831
848
|
|
|
832
849
|
Args:
|
|
833
|
-
options:
|
|
850
|
+
options: dict with:
|
|
834
851
|
- component_type: str - Which custom component to render (required)
|
|
835
852
|
- message: str - Message to display (required)
|
|
836
|
-
- data:
|
|
837
|
-
- actions:
|
|
853
|
+
- data: dict - Component-specific data (images, options, etc.)
|
|
854
|
+
- actions: list[dict] - Optional action buttons
|
|
838
855
|
- timeout: int - Timeout in seconds
|
|
839
856
|
- default: Any - Default value if timeout
|
|
840
857
|
- config_key: str - Reference to hitl: declaration
|
|
@@ -892,7 +909,7 @@ class HumanPrimitive:
|
|
|
892
909
|
"actions": actions,
|
|
893
910
|
}
|
|
894
911
|
|
|
895
|
-
logger.info(
|
|
912
|
+
logger.info("Human custom component requested: %s", component_type)
|
|
896
913
|
|
|
897
914
|
# CRITICAL: Wrap HITL call in checkpoint for transparent durability
|
|
898
915
|
def checkpoint_fn():
|