tactus 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.0.dist-info/METADATA +1809 -0
  157. tactus-0.31.0.dist-info/RECORD +160 -0
  158. tactus-0.31.0.dist-info/WHEEL +4 -0
  159. tactus-0.31.0.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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()"