tactus 0.34.1__py3-none-any.whl → 0.35.1__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 +40 -25
- 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.1.dist-info}/METADATA +15 -3
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ extracting declarations without executing code.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
import re
|
|
9
10
|
from typing import Any, Optional
|
|
10
11
|
|
|
11
12
|
from .generated.LuaParser import LuaParser
|
|
@@ -51,202 +52,207 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
51
52
|
|
|
52
53
|
def __init__(self):
|
|
53
54
|
self.builder = RegistryBuilder()
|
|
54
|
-
self.errors = []
|
|
55
|
-
self.warnings = []
|
|
55
|
+
self.errors: list[ValidationMessage] = []
|
|
56
|
+
self.warnings: list[ValidationMessage] = []
|
|
56
57
|
self.current_line = 0
|
|
57
58
|
self.current_col = 0
|
|
58
59
|
self.in_function_body = False # Track if we're inside a function body
|
|
59
60
|
|
|
60
|
-
def
|
|
61
|
+
def _record_error(
|
|
62
|
+
self,
|
|
63
|
+
message: str,
|
|
64
|
+
declaration: Optional[str] = None,
|
|
65
|
+
line_number: Optional[int] = None,
|
|
66
|
+
column_number: Optional[int] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Record a validation error with the current location by default."""
|
|
69
|
+
self.errors.append(
|
|
70
|
+
ValidationMessage(
|
|
71
|
+
level="error",
|
|
72
|
+
message=message,
|
|
73
|
+
location=(
|
|
74
|
+
self.current_line if line_number is None else line_number,
|
|
75
|
+
self.current_col if column_number is None else column_number,
|
|
76
|
+
),
|
|
77
|
+
declaration=declaration,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def visitFunctiondef(self, context):
|
|
61
82
|
"""Track when entering/exiting function definitions."""
|
|
62
83
|
# Set flag when entering function body
|
|
63
|
-
|
|
84
|
+
previous_in_function_body = self.in_function_body
|
|
64
85
|
self.in_function_body = True
|
|
65
86
|
try:
|
|
66
|
-
|
|
87
|
+
child_visit_result = super().visitChildren(context)
|
|
67
88
|
finally:
|
|
68
89
|
# Restore previous state when exiting
|
|
69
|
-
self.in_function_body =
|
|
70
|
-
return
|
|
90
|
+
self.in_function_body = previous_in_function_body
|
|
91
|
+
return child_visit_result
|
|
71
92
|
|
|
72
|
-
def visitStat(self,
|
|
93
|
+
def visitStat(self, context: LuaParser.StatContext):
|
|
73
94
|
"""Handle statement nodes including assignments."""
|
|
74
95
|
# Check if this is an assignment statement
|
|
75
|
-
if
|
|
96
|
+
if context.varlist() and context.explist():
|
|
76
97
|
# This is an assignment: varlist '=' explist
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
variable_list = context.varlist()
|
|
99
|
+
expression_list = context.explist()
|
|
79
100
|
|
|
80
101
|
# Get the variable name
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
|
|
102
|
+
if variable_list.var() and len(variable_list.var()) > 0:
|
|
103
|
+
assignment_target_node = variable_list.var()[0]
|
|
104
|
+
if assignment_target_node.NAME():
|
|
105
|
+
assignment_target_name = assignment_target_node.NAME().getText()
|
|
85
106
|
|
|
86
107
|
# Check if this is a DSL setting assignment
|
|
87
|
-
|
|
88
|
-
"default_provider",
|
|
89
|
-
"default_model",
|
|
90
|
-
"return_prompt",
|
|
91
|
-
"error_prompt",
|
|
92
|
-
"status_prompt",
|
|
93
|
-
"async",
|
|
94
|
-
"max_depth",
|
|
95
|
-
"max_turns",
|
|
96
|
-
|
|
108
|
+
setting_handlers_by_name = {
|
|
109
|
+
"default_provider": self.builder.set_default_provider,
|
|
110
|
+
"default_model": self.builder.set_default_model,
|
|
111
|
+
"return_prompt": self.builder.set_return_prompt,
|
|
112
|
+
"error_prompt": self.builder.set_error_prompt,
|
|
113
|
+
"status_prompt": self.builder.set_status_prompt,
|
|
114
|
+
"async": self.builder.set_async,
|
|
115
|
+
"max_depth": self.builder.set_max_depth,
|
|
116
|
+
"max_turns": self.builder.set_max_turns,
|
|
117
|
+
}
|
|
118
|
+
if assignment_target_name in setting_handlers_by_name:
|
|
97
119
|
# Get the value from explist
|
|
98
|
-
if
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
if expression_list.exp() and len(expression_list.exp()) > 0:
|
|
121
|
+
first_expression = expression_list.exp()[0]
|
|
122
|
+
literal_value = self._extract_literal_value(first_expression)
|
|
102
123
|
# Process the assignment like a function call
|
|
103
|
-
|
|
104
|
-
self.builder.set_default_provider(value)
|
|
105
|
-
elif var_name == "default_model":
|
|
106
|
-
self.builder.set_default_model(value)
|
|
107
|
-
elif var_name == "return_prompt":
|
|
108
|
-
self.builder.set_return_prompt(value)
|
|
109
|
-
elif var_name == "error_prompt":
|
|
110
|
-
self.builder.set_error_prompt(value)
|
|
111
|
-
elif var_name == "status_prompt":
|
|
112
|
-
self.builder.set_status_prompt(value)
|
|
113
|
-
elif var_name == "async":
|
|
114
|
-
self.builder.set_async(value)
|
|
115
|
-
elif var_name == "max_depth":
|
|
116
|
-
self.builder.set_max_depth(value)
|
|
117
|
-
elif var_name == "max_turns":
|
|
118
|
-
self.builder.set_max_turns(value)
|
|
124
|
+
setting_handlers_by_name[assignment_target_name](literal_value)
|
|
119
125
|
else:
|
|
120
126
|
# Check for assignment-based DSL declarations
|
|
121
127
|
# e.g., greeter = Agent {...}, done = Tool {...}
|
|
122
|
-
if
|
|
123
|
-
|
|
124
|
-
self._check_assignment_based_declaration(
|
|
128
|
+
if expression_list.exp() and len(expression_list.exp()) > 0:
|
|
129
|
+
first_expression = expression_list.exp()[0]
|
|
130
|
+
self._check_assignment_based_declaration(
|
|
131
|
+
assignment_target_name, first_expression
|
|
132
|
+
)
|
|
125
133
|
|
|
126
134
|
# Continue visiting children
|
|
127
|
-
return self.visitChildren(
|
|
135
|
+
return self.visitChildren(context)
|
|
128
136
|
|
|
129
|
-
def _check_assignment_based_declaration(
|
|
137
|
+
def _check_assignment_based_declaration(
|
|
138
|
+
self, assignment_target_name: str, assignment_expression
|
|
139
|
+
):
|
|
130
140
|
"""Check if an assignment is a DSL declaration like 'greeter = Agent {...}'."""
|
|
131
141
|
# Look for prefixexp with functioncall pattern: Agent {...}
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
if
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
if assignment_expression.prefixexp():
|
|
143
|
+
prefix_expression = assignment_expression.prefixexp()
|
|
144
|
+
if prefix_expression.functioncall():
|
|
145
|
+
function_call = prefix_expression.functioncall()
|
|
146
|
+
function_name = self._extract_function_name(function_call)
|
|
137
147
|
|
|
138
148
|
# Check if this is a chained method call (e.g., Agent('name').turn())
|
|
139
149
|
# Chained calls have structure: func_name args . method_name args
|
|
140
150
|
# Simple declarations have: func_name args or func_name table
|
|
141
151
|
# If there are more than 2 children, it's a chained call, not a declaration
|
|
142
|
-
|
|
152
|
+
is_chained_method_call = function_call.getChildCount() > 2
|
|
143
153
|
|
|
144
|
-
if
|
|
154
|
+
if function_name == "Agent" and not is_chained_method_call:
|
|
145
155
|
# Extract config from Agent {...}
|
|
146
|
-
|
|
156
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
147
157
|
# Filter out None values from tools list (variable refs can't be resolved)
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
if isinstance(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
if declaration_config and "tools" in declaration_config:
|
|
159
|
+
tool_name_list = declaration_config["tools"]
|
|
160
|
+
if isinstance(tool_name_list, list):
|
|
161
|
+
declaration_config["tools"] = [
|
|
162
|
+
tool_name for tool_name in tool_name_list if tool_name is not None
|
|
163
|
+
]
|
|
164
|
+
self.builder.register_agent(
|
|
165
|
+
assignment_target_name,
|
|
166
|
+
declaration_config if declaration_config else {},
|
|
167
|
+
None,
|
|
168
|
+
)
|
|
169
|
+
elif function_name == "Tool":
|
|
154
170
|
# Extract config from Tool {...}
|
|
155
|
-
|
|
171
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
156
172
|
if (
|
|
157
|
-
|
|
158
|
-
and isinstance(
|
|
159
|
-
and isinstance(
|
|
160
|
-
and
|
|
173
|
+
declaration_config
|
|
174
|
+
and isinstance(declaration_config, dict)
|
|
175
|
+
and isinstance(declaration_config.get("name"), str)
|
|
176
|
+
and declaration_config.get("name") != assignment_target_name
|
|
161
177
|
):
|
|
162
|
-
self.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
),
|
|
169
|
-
location=(self.current_line, self.current_col),
|
|
170
|
-
declaration="Tool",
|
|
171
|
-
)
|
|
178
|
+
self._record_error(
|
|
179
|
+
message=(
|
|
180
|
+
f"Tool name mismatch: '{assignment_target_name} = Tool {{ name = \"{declaration_config.get('name')}\" }}'. "
|
|
181
|
+
f"Remove the 'name' field or set it to '{assignment_target_name}'."
|
|
182
|
+
),
|
|
183
|
+
declaration="Tool",
|
|
172
184
|
)
|
|
173
|
-
self.builder.register_tool(
|
|
174
|
-
|
|
185
|
+
self.builder.register_tool(
|
|
186
|
+
assignment_target_name,
|
|
187
|
+
declaration_config if declaration_config else {},
|
|
188
|
+
None,
|
|
189
|
+
)
|
|
190
|
+
elif function_name == "Toolset":
|
|
175
191
|
# Extract config from Toolset {...}
|
|
176
|
-
|
|
177
|
-
self.builder.register_toolset(
|
|
178
|
-
|
|
192
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
193
|
+
self.builder.register_toolset(
|
|
194
|
+
assignment_target_name,
|
|
195
|
+
declaration_config if declaration_config else {},
|
|
196
|
+
)
|
|
197
|
+
elif function_name == "Procedure":
|
|
179
198
|
# New assignment syntax: main = Procedure { function(input) ... }
|
|
180
199
|
# Register as a named procedure
|
|
181
200
|
self.builder.register_named_procedure(
|
|
182
|
-
|
|
201
|
+
assignment_target_name,
|
|
183
202
|
None, # Function not available during validation
|
|
184
203
|
{}, # Input schema will be extracted from top-level input {}
|
|
185
204
|
{}, # Output schema will be extracted from top-level output {}
|
|
186
205
|
{}, # State schema
|
|
187
206
|
)
|
|
188
207
|
|
|
189
|
-
def _extract_single_table_arg(self,
|
|
208
|
+
def _extract_single_table_arg(self, function_call) -> dict:
|
|
190
209
|
"""Extract a single table argument from a function call like Agent {...}."""
|
|
191
|
-
|
|
192
|
-
if not
|
|
210
|
+
argument_list_nodes = function_call.args()
|
|
211
|
+
if not argument_list_nodes:
|
|
193
212
|
return {}
|
|
194
213
|
|
|
195
214
|
# Process first args entry only
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return self._parse_table_constructor(args_ctx.tableconstructor())
|
|
215
|
+
argument_context = argument_list_nodes[0]
|
|
216
|
+
if argument_context.tableconstructor():
|
|
217
|
+
return self._parse_table_constructor(argument_context.tableconstructor())
|
|
200
218
|
|
|
201
219
|
return {}
|
|
202
220
|
|
|
203
|
-
def visitFunctioncall(self,
|
|
221
|
+
def visitFunctioncall(self, context: LuaParser.FunctioncallContext):
|
|
204
222
|
"""Recognize and process DSL function calls."""
|
|
205
223
|
try:
|
|
206
224
|
# Extract line/column for error reporting
|
|
207
|
-
if
|
|
208
|
-
self.current_line =
|
|
209
|
-
self.current_col =
|
|
225
|
+
if context.start:
|
|
226
|
+
self.current_line = context.start.line
|
|
227
|
+
self.current_col = context.start.column
|
|
210
228
|
|
|
211
229
|
# Check for deprecated method calls like .turn() or .run()
|
|
212
|
-
self._check_deprecated_method_calls(
|
|
230
|
+
self._check_deprecated_method_calls(context)
|
|
213
231
|
|
|
214
|
-
|
|
232
|
+
function_name = self._extract_function_name(context)
|
|
215
233
|
|
|
216
234
|
# Check if this is a method call (e.g., Tool.called()) vs a direct call (e.g., Tool())
|
|
217
235
|
# For "Tool.called()", parser extracts "Tool" as func_name but full text is "Tool.called(...)"
|
|
218
236
|
# We want to skip if full_text shows it's actually calling a method ON Tool, not Tool itself
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if
|
|
222
|
-
# If the text is "Tool.called(...)" and func_name is "Tool",
|
|
223
|
-
# then it's actually calling .called() method on Tool, not calling Tool()
|
|
224
|
-
# Check: does full_text have func_name followed by a dot/colon (not by opening paren)?
|
|
225
|
-
# Pattern: func_name followed by . or : means it's accessing a method/property
|
|
226
|
-
import re
|
|
227
|
-
|
|
228
|
-
# Match: funcName followed by . or : (not by opening paren directly)
|
|
229
|
-
method_access_pattern = re.escape(func_name) + r"[.:]"
|
|
230
|
-
if re.search(method_access_pattern, full_text):
|
|
231
|
-
is_method_call = True
|
|
232
|
-
|
|
233
|
-
if func_name in self.DSL_FUNCTIONS and not is_method_call:
|
|
237
|
+
is_method_access_call = self._is_method_access_call(function_name, context)
|
|
238
|
+
|
|
239
|
+
if function_name in self.DSL_FUNCTIONS and not is_method_access_call:
|
|
234
240
|
# Process the DSL call (but skip method calls like Tool.called())
|
|
235
241
|
try:
|
|
236
|
-
self._process_dsl_call(
|
|
237
|
-
except Exception as
|
|
242
|
+
self._process_dsl_call(function_name, context)
|
|
243
|
+
except Exception as processing_exception:
|
|
238
244
|
self.errors.append(
|
|
239
245
|
ValidationMessage(
|
|
240
246
|
level="error",
|
|
241
|
-
message=f"Error processing {
|
|
247
|
+
message=(f"Error processing {function_name}: {processing_exception}"),
|
|
242
248
|
location=(self.current_line, self.current_col),
|
|
243
|
-
declaration=
|
|
249
|
+
declaration=function_name,
|
|
244
250
|
)
|
|
245
251
|
)
|
|
246
|
-
except Exception as
|
|
247
|
-
logger.debug(
|
|
252
|
+
except Exception as visit_exception:
|
|
253
|
+
logger.debug("Error in visitFunctioncall: %s", visit_exception)
|
|
248
254
|
|
|
249
|
-
return self.visitChildren(
|
|
255
|
+
return self.visitChildren(context)
|
|
250
256
|
|
|
251
257
|
def _check_deprecated_method_calls(self, ctx: LuaParser.FunctioncallContext):
|
|
252
258
|
"""Check for deprecated method calls like .turn() or .run()."""
|
|
@@ -259,13 +265,12 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
259
265
|
|
|
260
266
|
# Check for .turn() pattern (method call with dot notation)
|
|
261
267
|
if ".turn(" in full_text or ":turn(" in full_text:
|
|
262
|
-
self.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)
|
|
268
|
+
self._record_error(
|
|
269
|
+
message=(
|
|
270
|
+
"The .turn() method is deprecated. Use callable syntax instead: "
|
|
271
|
+
'agent() or agent({message = "..."})'
|
|
272
|
+
),
|
|
273
|
+
declaration="Agent.turn",
|
|
269
274
|
)
|
|
270
275
|
|
|
271
276
|
# Check for .run() pattern on agents
|
|
@@ -273,75 +278,92 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
273
278
|
# Try to determine if this is an agent call (not procedure or other types)
|
|
274
279
|
# If the text contains "Agent(" it's likely an agent method
|
|
275
280
|
if "Agent(" in full_text or ctx.getText().startswith("agent"):
|
|
276
|
-
self.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
)
|
|
281
|
+
self._record_error(
|
|
282
|
+
message=(
|
|
283
|
+
"The .run() method on agents is deprecated. Use callable syntax instead: "
|
|
284
|
+
'agent() or agent({message = "..."})'
|
|
285
|
+
),
|
|
286
|
+
declaration="Agent.run",
|
|
283
287
|
)
|
|
284
288
|
|
|
285
|
-
def
|
|
289
|
+
def _is_method_access_call(
|
|
290
|
+
self, function_name: Optional[str], ctx: LuaParser.FunctioncallContext
|
|
291
|
+
) -> bool:
|
|
292
|
+
"""Return True when the call is actually a method access like Tool.called()."""
|
|
293
|
+
if not function_name:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
function_call_text = ctx.getText()
|
|
297
|
+
method_access_pattern = re.escape(function_name) + r"[.:]"
|
|
298
|
+
return re.search(method_access_pattern, function_call_text) is not None
|
|
299
|
+
|
|
300
|
+
def _extract_literal_value(self, expression_node):
|
|
286
301
|
"""Extract a literal value from an expression node."""
|
|
287
|
-
if not
|
|
302
|
+
if not expression_node:
|
|
288
303
|
return None
|
|
289
304
|
|
|
305
|
+
expression_text = expression_node.getText()
|
|
306
|
+
|
|
290
307
|
# Check for string literals
|
|
291
|
-
if
|
|
292
|
-
|
|
308
|
+
if expression_node.string():
|
|
309
|
+
string_context = expression_node.string()
|
|
293
310
|
# Extract the string value (remove quotes)
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
# Remove surrounding quotes
|
|
297
|
-
if text.startswith('"') and text.endswith('"'):
|
|
298
|
-
return text[1:-1]
|
|
299
|
-
elif text.startswith("'") and text.endswith("'"):
|
|
300
|
-
return text[1:-1]
|
|
301
|
-
elif string_ctx.CHARSTRING():
|
|
302
|
-
text = string_ctx.CHARSTRING().getText()
|
|
311
|
+
if string_context.NORMALSTRING():
|
|
312
|
+
string_text = string_context.NORMALSTRING().getText()
|
|
303
313
|
# Remove surrounding quotes
|
|
304
|
-
if
|
|
305
|
-
return
|
|
306
|
-
elif
|
|
307
|
-
return
|
|
314
|
+
if string_text.startswith('"') and string_text.endswith('"'):
|
|
315
|
+
return string_text[1:-1]
|
|
316
|
+
elif string_text.startswith("'") and string_text.endswith("'"):
|
|
317
|
+
return string_text[1:-1]
|
|
318
|
+
elif string_context.CHARSTRING():
|
|
319
|
+
string_text = string_context.CHARSTRING().getText()
|
|
320
|
+
# Character strings are always quoted in Lua tokens.
|
|
321
|
+
if (
|
|
322
|
+
len(string_text) >= 2
|
|
323
|
+
and string_text[0] == string_text[-1]
|
|
324
|
+
and string_text[0] in ("'", '"')
|
|
325
|
+
):
|
|
326
|
+
return string_text[1:-1]
|
|
327
|
+
return string_text
|
|
308
328
|
|
|
309
329
|
# Check for number literals
|
|
310
|
-
if
|
|
311
|
-
|
|
312
|
-
if
|
|
313
|
-
return int(
|
|
314
|
-
elif
|
|
315
|
-
return float(
|
|
330
|
+
if expression_node.number():
|
|
331
|
+
number_context = expression_node.number()
|
|
332
|
+
if number_context.INT():
|
|
333
|
+
return int(number_context.INT().getText())
|
|
334
|
+
elif number_context.FLOAT():
|
|
335
|
+
return float(number_context.FLOAT().getText())
|
|
316
336
|
|
|
317
337
|
# Check for boolean literals
|
|
318
|
-
if
|
|
338
|
+
if expression_text == "true":
|
|
319
339
|
return True
|
|
320
|
-
elif
|
|
340
|
+
elif expression_text == "false":
|
|
321
341
|
return False
|
|
322
342
|
|
|
323
343
|
# Check for nil
|
|
324
|
-
if
|
|
344
|
+
if expression_text == "nil":
|
|
325
345
|
return None
|
|
326
346
|
|
|
327
347
|
# Default to the text representation
|
|
328
|
-
return
|
|
348
|
+
return expression_text
|
|
329
349
|
|
|
330
|
-
def _extract_function_name(
|
|
350
|
+
def _extract_function_name(
|
|
351
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
352
|
+
) -> Optional[str]:
|
|
331
353
|
"""Extract function name from parse tree."""
|
|
332
354
|
# The function name is the first child of functioncall
|
|
333
355
|
# Look for a terminal node with text
|
|
334
|
-
for i in range(
|
|
335
|
-
child =
|
|
356
|
+
for i in range(function_call_context.getChildCount()):
|
|
357
|
+
child = function_call_context.getChild(i)
|
|
336
358
|
if hasattr(child, "symbol"):
|
|
337
359
|
# It's a terminal node
|
|
338
|
-
|
|
339
|
-
if
|
|
340
|
-
return
|
|
360
|
+
token_text = child.getText()
|
|
361
|
+
if token_text and token_text.isidentifier():
|
|
362
|
+
return token_text
|
|
341
363
|
|
|
342
364
|
# Fallback: try varOrExp approach
|
|
343
|
-
if
|
|
344
|
-
var_or_exp =
|
|
365
|
+
if function_call_context.varOrExp():
|
|
366
|
+
var_or_exp = function_call_context.varOrExp()
|
|
345
367
|
# varOrExp: var | '(' exp ')'
|
|
346
368
|
if var_or_exp.var():
|
|
347
369
|
var_ctx = var_or_exp.var()
|
|
@@ -351,87 +373,96 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
351
373
|
|
|
352
374
|
return None
|
|
353
375
|
|
|
354
|
-
def _process_dsl_call(self,
|
|
376
|
+
def _process_dsl_call(self, function_name: str, ctx: LuaParser.FunctioncallContext):
|
|
355
377
|
"""Extract arguments and register declaration."""
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if
|
|
359
|
-
if
|
|
360
|
-
self.builder.set_name(
|
|
361
|
-
elif
|
|
362
|
-
if
|
|
363
|
-
self.builder.set_version(
|
|
364
|
-
elif
|
|
378
|
+
argument_values = self._extract_arguments(ctx)
|
|
379
|
+
|
|
380
|
+
if function_name == "name":
|
|
381
|
+
if argument_values and len(argument_values) >= 1:
|
|
382
|
+
self.builder.set_name(argument_values[0])
|
|
383
|
+
elif function_name == "version":
|
|
384
|
+
if argument_values and len(argument_values) >= 1:
|
|
385
|
+
self.builder.set_version(argument_values[0])
|
|
386
|
+
elif function_name == "Agent": # CamelCase only
|
|
365
387
|
# Skip Agent calls inside function bodies - they're runtime lookups, not declarations
|
|
366
388
|
if self.in_function_body:
|
|
367
389
|
return self.visitChildren(ctx)
|
|
368
390
|
|
|
369
|
-
if
|
|
370
|
-
|
|
391
|
+
if (
|
|
392
|
+
argument_values and len(argument_values) >= 1
|
|
393
|
+
): # Support curried syntax with just name
|
|
394
|
+
agent_name = argument_values[0]
|
|
371
395
|
# Check if this is a declaration (has config) or a lookup (just name)
|
|
372
|
-
if len(
|
|
396
|
+
if len(argument_values) >= 2 and isinstance(argument_values[1], dict):
|
|
373
397
|
# DEPRECATED: Curried syntax Agent "name" { config }
|
|
374
398
|
# Raise validation error
|
|
375
399
|
self.errors.append(
|
|
376
400
|
ValidationMessage(
|
|
377
401
|
level="error",
|
|
378
|
-
message=
|
|
402
|
+
message=(
|
|
403
|
+
f'Curried syntax Agent "{agent_name}" {{...}} is deprecated. '
|
|
404
|
+
f"Use assignment syntax: {agent_name} = Agent {{...}}"
|
|
405
|
+
),
|
|
379
406
|
location=(self.current_line, self.current_col),
|
|
380
407
|
declaration="Agent",
|
|
381
408
|
)
|
|
382
409
|
)
|
|
383
|
-
elif len(
|
|
410
|
+
elif len(argument_values) == 1 and isinstance(agent_name, str):
|
|
384
411
|
# DEPRECATED: Agent("name") lookup or curried declaration
|
|
385
412
|
# This is now invalid - users should use variable references
|
|
386
|
-
self.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
self._record_error(
|
|
414
|
+
message=(
|
|
415
|
+
f'Agent("{agent_name}") lookup syntax is deprecated. '
|
|
416
|
+
f"Declare the agent with assignment: {agent_name} = Agent {{...}}, "
|
|
417
|
+
f"then use {agent_name}() to call it."
|
|
418
|
+
),
|
|
419
|
+
declaration="Agent",
|
|
393
420
|
)
|
|
394
|
-
elif
|
|
395
|
-
if
|
|
421
|
+
elif function_name == "Model": # CamelCase only
|
|
422
|
+
if argument_values and len(argument_values) >= 1:
|
|
396
423
|
# Check if this is assignment syntax (single dict arg) or curried syntax (name + dict)
|
|
397
|
-
if len(
|
|
424
|
+
if len(argument_values) == 1 and isinstance(argument_values[0], dict):
|
|
398
425
|
# Assignment syntax: my_model = Model {config}
|
|
399
426
|
# Generate a temp name for validation
|
|
400
427
|
import uuid
|
|
401
428
|
|
|
402
429
|
temp_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
|
|
403
|
-
self.builder.register_model(temp_name,
|
|
404
|
-
elif len(
|
|
430
|
+
self.builder.register_model(temp_name, argument_values[0])
|
|
431
|
+
elif len(argument_values) >= 2 and isinstance(argument_values[1], dict):
|
|
405
432
|
# Curried syntax: Model "name" {config}
|
|
406
|
-
config =
|
|
407
|
-
self.builder.register_model(
|
|
408
|
-
elif isinstance(
|
|
433
|
+
config = argument_values[1]
|
|
434
|
+
self.builder.register_model(argument_values[0], config)
|
|
435
|
+
elif isinstance(argument_values[0], str):
|
|
409
436
|
# Just a name, register with empty config
|
|
410
|
-
self.builder.register_model(
|
|
411
|
-
elif
|
|
437
|
+
self.builder.register_model(argument_values[0], {})
|
|
438
|
+
elif function_name == "Procedure": # CamelCase only
|
|
412
439
|
# Supports multiple syntax variants:
|
|
413
440
|
# 1. Unnamed (new): Procedure { config with function }
|
|
414
441
|
# 2. Named (curried): Procedure "name" { config }
|
|
415
442
|
# 3. Named (old): procedure("name", {config}, function)
|
|
416
443
|
# Note: args may contain None for unparseable expressions (like functions)
|
|
417
|
-
if
|
|
444
|
+
if argument_values and len(argument_values) >= 1:
|
|
418
445
|
# Check if first arg is a table (unnamed procedure syntax)
|
|
419
446
|
# Tables are parsed as dict if they have named fields, or list if only positional
|
|
420
|
-
if isinstance(
|
|
447
|
+
if isinstance(argument_values[0], dict):
|
|
421
448
|
# Unnamed syntax: Procedure {...} with named fields
|
|
422
449
|
# e.g., Procedure { output = {...}, function(input) ... end }
|
|
423
450
|
proc_name = "main"
|
|
424
|
-
config =
|
|
425
|
-
elif isinstance(
|
|
451
|
+
config = argument_values[0]
|
|
452
|
+
elif isinstance(argument_values[0], list):
|
|
426
453
|
# Unnamed syntax: Procedure {...} with only function (no named fields)
|
|
427
454
|
# e.g., Procedure { function(input) ... end }
|
|
428
455
|
# The list contains [None] for the unparseable function
|
|
429
456
|
proc_name = "main"
|
|
430
457
|
config = {} # No extractable config from function-only table
|
|
431
|
-
elif isinstance(
|
|
458
|
+
elif isinstance(argument_values[0], str):
|
|
432
459
|
# Named syntax: Procedure "name" {...}
|
|
433
|
-
proc_name =
|
|
434
|
-
config =
|
|
460
|
+
proc_name = argument_values[0]
|
|
461
|
+
config = (
|
|
462
|
+
argument_values[1]
|
|
463
|
+
if len(argument_values) >= 2 and isinstance(argument_values[1], dict)
|
|
464
|
+
else None
|
|
465
|
+
)
|
|
435
466
|
else:
|
|
436
467
|
# Invalid syntax
|
|
437
468
|
return
|
|
@@ -459,109 +490,134 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
459
490
|
# Extract inline state schema
|
|
460
491
|
if "state" in config and isinstance(config["state"], dict):
|
|
461
492
|
self.builder.register_state_schema(config["state"])
|
|
462
|
-
elif
|
|
463
|
-
if
|
|
464
|
-
self.builder.register_prompt(
|
|
465
|
-
elif
|
|
466
|
-
if
|
|
467
|
-
self.builder.register_hitl(
|
|
468
|
-
|
|
493
|
+
elif function_name == "Prompt": # CamelCase
|
|
494
|
+
if argument_values and len(argument_values) >= 2:
|
|
495
|
+
self.builder.register_prompt(argument_values[0], argument_values[1])
|
|
496
|
+
elif function_name == "Hitl": # CamelCase
|
|
497
|
+
if argument_values and len(argument_values) >= 2:
|
|
498
|
+
self.builder.register_hitl(
|
|
499
|
+
argument_values[0],
|
|
500
|
+
argument_values[1] if isinstance(argument_values[1], dict) else {},
|
|
501
|
+
)
|
|
502
|
+
elif function_name == "Specification": # CamelCase
|
|
469
503
|
# Three supported forms:
|
|
470
504
|
# - Specification([[ Gherkin text ]]) (inline Gherkin)
|
|
471
505
|
# - Specification("name", { ... }) (structured form; legacy)
|
|
472
506
|
# - Specification { from = "path" } (external file reference)
|
|
473
|
-
if
|
|
474
|
-
|
|
475
|
-
if isinstance(
|
|
507
|
+
if argument_values and len(argument_values) == 1:
|
|
508
|
+
specification_argument = argument_values[0]
|
|
509
|
+
if isinstance(specification_argument, dict) and "from" in specification_argument:
|
|
476
510
|
# External file reference
|
|
477
|
-
self.builder.register_specs_from(
|
|
511
|
+
self.builder.register_specs_from(specification_argument["from"])
|
|
478
512
|
else:
|
|
479
513
|
# Inline Gherkin text
|
|
480
|
-
self.builder.register_specifications(
|
|
481
|
-
elif
|
|
514
|
+
self.builder.register_specifications(specification_argument)
|
|
515
|
+
elif argument_values and len(argument_values) >= 2:
|
|
482
516
|
self.builder.register_specification(
|
|
483
|
-
|
|
517
|
+
argument_values[0],
|
|
518
|
+
argument_values[1] if isinstance(argument_values[1], list) else [],
|
|
484
519
|
)
|
|
485
|
-
elif
|
|
520
|
+
elif function_name == "Specifications": # CamelCase
|
|
486
521
|
# Specifications([[ Gherkin text ]]) (plural form; singular is Specification([[...]]))
|
|
487
|
-
if
|
|
488
|
-
self.builder.register_specifications(
|
|
489
|
-
elif
|
|
522
|
+
if argument_values and len(argument_values) >= 1:
|
|
523
|
+
self.builder.register_specifications(argument_values[0])
|
|
524
|
+
elif function_name == "Step": # CamelCase
|
|
490
525
|
# Step("step text", function() ... end)
|
|
491
|
-
if
|
|
492
|
-
self.builder.register_custom_step(
|
|
493
|
-
elif
|
|
526
|
+
if argument_values and len(argument_values) >= 2:
|
|
527
|
+
self.builder.register_custom_step(argument_values[0], argument_values[1])
|
|
528
|
+
elif function_name == "Evaluation": # CamelCase
|
|
494
529
|
# Either:
|
|
495
530
|
# - Evaluation({ runs = 10, parallel = true }) (simple config)
|
|
496
531
|
# - Evaluation({ dataset = {...}, evaluators = {...}, ... }) (alias for Evaluations)
|
|
497
|
-
if
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
532
|
+
if (
|
|
533
|
+
argument_values
|
|
534
|
+
and len(argument_values) >= 1
|
|
535
|
+
and isinstance(argument_values[0], dict)
|
|
536
|
+
):
|
|
537
|
+
evaluation_config = argument_values[0]
|
|
538
|
+
if any(
|
|
539
|
+
key in evaluation_config
|
|
540
|
+
for key in ("dataset", "dataset_file", "evaluators", "thresholds")
|
|
541
|
+
):
|
|
542
|
+
self.builder.register_evaluations(evaluation_config)
|
|
501
543
|
else:
|
|
502
|
-
self.builder.set_evaluation_config(
|
|
503
|
-
elif
|
|
544
|
+
self.builder.set_evaluation_config(evaluation_config)
|
|
545
|
+
elif argument_values and len(argument_values) >= 1:
|
|
504
546
|
self.builder.set_evaluation_config({})
|
|
505
|
-
elif
|
|
547
|
+
elif function_name == "Evaluations": # CamelCase
|
|
506
548
|
# Evaluation(s)({ dataset = {...}, evaluators = {...} })
|
|
507
|
-
if
|
|
508
|
-
self.builder.register_evaluations(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
549
|
+
if argument_values and len(argument_values) >= 1:
|
|
550
|
+
self.builder.register_evaluations(
|
|
551
|
+
argument_values[0] if isinstance(argument_values[0], dict) else {}
|
|
552
|
+
)
|
|
553
|
+
elif function_name == "default_provider":
|
|
554
|
+
if argument_values and len(argument_values) >= 1:
|
|
555
|
+
self.builder.set_default_provider(argument_values[0])
|
|
556
|
+
elif function_name == "default_model":
|
|
557
|
+
if argument_values and len(argument_values) >= 1:
|
|
558
|
+
self.builder.set_default_model(argument_values[0])
|
|
559
|
+
elif function_name == "return_prompt":
|
|
560
|
+
if argument_values and len(argument_values) >= 1:
|
|
561
|
+
self.builder.set_return_prompt(argument_values[0])
|
|
562
|
+
elif function_name == "error_prompt":
|
|
563
|
+
if argument_values and len(argument_values) >= 1:
|
|
564
|
+
self.builder.set_error_prompt(argument_values[0])
|
|
565
|
+
elif function_name == "status_prompt":
|
|
566
|
+
if argument_values and len(argument_values) >= 1:
|
|
567
|
+
self.builder.set_status_prompt(argument_values[0])
|
|
568
|
+
elif function_name == "async":
|
|
569
|
+
if argument_values and len(argument_values) >= 1:
|
|
570
|
+
self.builder.set_async(argument_values[0])
|
|
571
|
+
elif function_name == "max_depth":
|
|
572
|
+
if argument_values and len(argument_values) >= 1:
|
|
573
|
+
self.builder.set_max_depth(argument_values[0])
|
|
574
|
+
elif function_name == "max_turns":
|
|
575
|
+
if argument_values and len(argument_values) >= 1:
|
|
576
|
+
self.builder.set_max_turns(argument_values[0])
|
|
577
|
+
elif function_name == "input":
|
|
534
578
|
# Top-level input schema for script mode: input { field1 = ..., field2 = ... }
|
|
535
|
-
if
|
|
536
|
-
|
|
537
|
-
|
|
579
|
+
if (
|
|
580
|
+
argument_values
|
|
581
|
+
and len(argument_values) >= 1
|
|
582
|
+
and isinstance(argument_values[0], dict)
|
|
583
|
+
):
|
|
584
|
+
self.builder.register_top_level_input(argument_values[0])
|
|
585
|
+
elif function_name == "output":
|
|
538
586
|
# Top-level output schema for script mode: output { field1 = ..., field2 = ... }
|
|
539
|
-
if
|
|
540
|
-
|
|
541
|
-
|
|
587
|
+
if (
|
|
588
|
+
argument_values
|
|
589
|
+
and len(argument_values) >= 1
|
|
590
|
+
and isinstance(argument_values[0], dict)
|
|
591
|
+
):
|
|
592
|
+
self.builder.register_top_level_output(argument_values[0])
|
|
593
|
+
elif function_name == "Tool": # CamelCase only
|
|
542
594
|
# Curried syntax (Tool "name" {...} / Tool("name", ...)) is not supported.
|
|
543
595
|
# Use assignment syntax: my_tool = Tool { ... }.
|
|
544
|
-
if
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
596
|
+
if (
|
|
597
|
+
argument_values
|
|
598
|
+
and len(argument_values) >= 1
|
|
599
|
+
and isinstance(argument_values[0], str)
|
|
600
|
+
):
|
|
601
|
+
tool_name = argument_values[0]
|
|
602
|
+
self._record_error(
|
|
603
|
+
message=(
|
|
604
|
+
f'Curried Tool syntax is not supported: Tool "{tool_name}" {{...}}. '
|
|
605
|
+
f"Use assignment syntax: {tool_name} = Tool {{...}}."
|
|
606
|
+
),
|
|
607
|
+
declaration="Tool",
|
|
556
608
|
)
|
|
557
|
-
elif
|
|
609
|
+
elif function_name == "Toolset": # CamelCase only
|
|
558
610
|
# Toolset("name", {config})
|
|
559
611
|
# or new curried syntax: Toolset "name" { config }
|
|
560
|
-
if
|
|
612
|
+
if argument_values and len(argument_values) >= 1: # Support curried syntax
|
|
561
613
|
# First arg must be name (string)
|
|
562
|
-
if isinstance(
|
|
563
|
-
toolset_name =
|
|
564
|
-
config =
|
|
614
|
+
if isinstance(argument_values[0], str):
|
|
615
|
+
toolset_name = argument_values[0]
|
|
616
|
+
config = (
|
|
617
|
+
argument_values[1]
|
|
618
|
+
if len(argument_values) >= 2 and isinstance(argument_values[1], dict)
|
|
619
|
+
else {}
|
|
620
|
+
)
|
|
565
621
|
# Register the toolset (validation only, no runtime impl yet)
|
|
566
622
|
self.builder.register_toolset(toolset_name, config)
|
|
567
623
|
|
|
@@ -573,20 +629,20 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
573
629
|
- Unparseable expressions (like functions) are included as None placeholders
|
|
574
630
|
This allows checking total argument count for validation.
|
|
575
631
|
"""
|
|
576
|
-
|
|
632
|
+
parsed_arguments = []
|
|
577
633
|
|
|
578
634
|
# functioncall has args() children
|
|
579
635
|
# args: '(' explist? ')' | tableconstructor | LiteralString
|
|
580
636
|
|
|
581
|
-
|
|
582
|
-
if not
|
|
583
|
-
return
|
|
637
|
+
argument_nodes = ctx.args()
|
|
638
|
+
if not argument_nodes:
|
|
639
|
+
return parsed_arguments
|
|
584
640
|
|
|
585
641
|
# Check if this is a method call chain by looking for '.' or ':' between args
|
|
586
642
|
# For Agent("name").turn({...}), we should only extract "name"
|
|
587
643
|
# For Procedure "name" {...}, we should extract both "name" and {...}
|
|
588
644
|
is_method_chain = False
|
|
589
|
-
if len(
|
|
645
|
+
if len(argument_nodes) > 1:
|
|
590
646
|
# Check if there's a method access between the first two args
|
|
591
647
|
# Method chains have pattern: func(arg1).method(arg2)
|
|
592
648
|
# Shorthand has pattern: func arg1 arg2
|
|
@@ -597,9 +653,9 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
597
653
|
for i in range(ctx.getChildCount()):
|
|
598
654
|
child = ctx.getChild(i)
|
|
599
655
|
# Check if this is the first args
|
|
600
|
-
if child ==
|
|
656
|
+
if child == argument_nodes[0]:
|
|
601
657
|
found_first_args = True
|
|
602
|
-
elif found_first_args and child ==
|
|
658
|
+
elif found_first_args and child == argument_nodes[1]:
|
|
603
659
|
# We've reached the second args without finding . or :
|
|
604
660
|
# So this is NOT a method chain
|
|
605
661
|
break
|
|
@@ -613,30 +669,30 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
613
669
|
# Process arguments
|
|
614
670
|
if is_method_chain:
|
|
615
671
|
# Only process first args for method chains like Agent("name").turn(...)
|
|
616
|
-
args_to_process = [
|
|
672
|
+
args_to_process = [argument_nodes[0]]
|
|
617
673
|
else:
|
|
618
674
|
# Process all args for shorthand syntax like Procedure "name" {...}
|
|
619
|
-
args_to_process =
|
|
675
|
+
args_to_process = argument_nodes
|
|
620
676
|
|
|
621
677
|
for args_ctx in args_to_process:
|
|
622
678
|
# Check for different argument types
|
|
623
679
|
if args_ctx.explist():
|
|
624
680
|
# Regular function call with expression list
|
|
625
|
-
|
|
626
|
-
for
|
|
627
|
-
value = self._parse_expression(
|
|
681
|
+
expression_list = args_ctx.explist()
|
|
682
|
+
for expression in expression_list.exp():
|
|
683
|
+
value = self._parse_expression(expression)
|
|
628
684
|
# Include None placeholders to preserve argument count
|
|
629
|
-
|
|
685
|
+
parsed_arguments.append(value)
|
|
630
686
|
elif args_ctx.tableconstructor():
|
|
631
687
|
# Table constructor argument
|
|
632
688
|
table = self._parse_table_constructor(args_ctx.tableconstructor())
|
|
633
|
-
|
|
689
|
+
parsed_arguments.append(table)
|
|
634
690
|
elif args_ctx.string():
|
|
635
691
|
# String literal argument
|
|
636
692
|
string_val = self._parse_string(args_ctx.string())
|
|
637
|
-
|
|
693
|
+
parsed_arguments.append(string_val)
|
|
638
694
|
|
|
639
|
-
return
|
|
695
|
+
return parsed_arguments
|
|
640
696
|
|
|
641
697
|
def _parse_expression(self, ctx: LuaParser.ExpContext) -> Any:
|
|
642
698
|
"""Parse an expression to a Python value."""
|
|
@@ -644,22 +700,22 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
644
700
|
return None
|
|
645
701
|
|
|
646
702
|
# Detect field.<type>{...} builder syntax so we can preserve schema info
|
|
647
|
-
|
|
648
|
-
if
|
|
649
|
-
|
|
650
|
-
|
|
703
|
+
prefix_expression = ctx.prefixexp()
|
|
704
|
+
if prefix_expression and prefix_expression.functioncall():
|
|
705
|
+
function_call_context = prefix_expression.functioncall()
|
|
706
|
+
function_name_tokens = [token.getText() for token in function_call_context.NAME()]
|
|
651
707
|
|
|
652
708
|
# field.string{required = true, ...}
|
|
653
|
-
if len(
|
|
654
|
-
field_type =
|
|
709
|
+
if len(function_name_tokens) >= 2 and function_name_tokens[0] == "field":
|
|
710
|
+
field_type = function_name_tokens[-1]
|
|
655
711
|
|
|
656
712
|
# Default field definition
|
|
657
713
|
field_def = {"type": field_type, "required": False}
|
|
658
714
|
|
|
659
715
|
# Parse options table if present
|
|
660
|
-
if
|
|
716
|
+
if function_call_context.args():
|
|
661
717
|
# We only expect a single args() entry for the builder
|
|
662
|
-
first_arg =
|
|
718
|
+
first_arg = function_call_context.args(0)
|
|
663
719
|
if first_arg.tableconstructor():
|
|
664
720
|
options = self._parse_table_constructor(first_arg.tableconstructor())
|
|
665
721
|
if isinstance(options, dict):
|
|
@@ -707,42 +763,42 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
707
763
|
|
|
708
764
|
def _parse_string_token(self, token) -> str:
|
|
709
765
|
"""Parse string token to Python string."""
|
|
710
|
-
|
|
766
|
+
token_text = token.getText()
|
|
711
767
|
|
|
712
768
|
# Handle different Lua string formats
|
|
713
|
-
if
|
|
769
|
+
if token_text.startswith("[[") and token_text.endswith("]]"):
|
|
714
770
|
# Long string literal
|
|
715
|
-
return
|
|
716
|
-
elif
|
|
771
|
+
return token_text[2:-2]
|
|
772
|
+
elif token_text.startswith('"') and token_text.endswith('"'):
|
|
717
773
|
# Double-quoted string
|
|
718
|
-
content =
|
|
774
|
+
content = token_text[1:-1]
|
|
719
775
|
content = content.replace("\\n", "\n")
|
|
720
776
|
content = content.replace("\\t", "\t")
|
|
721
777
|
content = content.replace('\\"', '"')
|
|
722
778
|
content = content.replace("\\\\", "\\")
|
|
723
779
|
return content
|
|
724
|
-
elif
|
|
780
|
+
elif token_text.startswith("'") and token_text.endswith("'"):
|
|
725
781
|
# Single-quoted string
|
|
726
|
-
content =
|
|
782
|
+
content = token_text[1:-1]
|
|
727
783
|
content = content.replace("\\n", "\n")
|
|
728
784
|
content = content.replace("\\t", "\t")
|
|
729
785
|
content = content.replace("\\'", "'")
|
|
730
786
|
content = content.replace("\\\\", "\\")
|
|
731
787
|
return content
|
|
732
788
|
|
|
733
|
-
return
|
|
789
|
+
return token_text
|
|
734
790
|
|
|
735
|
-
def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) ->
|
|
791
|
+
def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) -> Any:
|
|
736
792
|
"""Parse Lua table constructor to Python dict."""
|
|
737
|
-
|
|
793
|
+
parsed_table = {}
|
|
738
794
|
array_items = []
|
|
739
795
|
|
|
740
796
|
if not ctx or not ctx.fieldlist():
|
|
741
797
|
# Empty table
|
|
742
798
|
return [] # Return empty list for empty tables (matches runtime behavior)
|
|
743
799
|
|
|
744
|
-
|
|
745
|
-
for field in
|
|
800
|
+
field_list = ctx.fieldlist()
|
|
801
|
+
for field in field_list.field():
|
|
746
802
|
# field: '[' exp ']' '=' exp | NAME '=' exp | exp
|
|
747
803
|
if field.NAME():
|
|
748
804
|
# Named field: NAME '=' exp
|
|
@@ -769,13 +825,16 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
769
825
|
self.errors.append(
|
|
770
826
|
ValidationMessage(
|
|
771
827
|
level="error",
|
|
772
|
-
message=
|
|
828
|
+
message=(
|
|
829
|
+
f"Old type syntax detected. Use field.{value}{{}} instead of "
|
|
830
|
+
f"{{type = '{value}'}}"
|
|
831
|
+
),
|
|
773
832
|
line=field.start.line if field.start else 0,
|
|
774
833
|
column=field.start.column if field.start else 0,
|
|
775
834
|
)
|
|
776
835
|
)
|
|
777
836
|
|
|
778
|
-
|
|
837
|
+
parsed_table[key] = value
|
|
779
838
|
elif len(field.exp()) == 2:
|
|
780
839
|
# Indexed field: '[' exp ']' '=' exp
|
|
781
840
|
# Skip for now (complex)
|
|
@@ -786,37 +845,37 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
786
845
|
array_items.append(value)
|
|
787
846
|
|
|
788
847
|
# If we only have array items, return as list
|
|
789
|
-
if array_items and not
|
|
848
|
+
if array_items and not parsed_table:
|
|
790
849
|
return array_items
|
|
791
850
|
|
|
792
851
|
# If we have both, prefer dict (shouldn't happen in DSL)
|
|
793
852
|
if array_items:
|
|
794
853
|
# Mixed table - add array items with numeric keys
|
|
795
854
|
for i, item in enumerate(array_items, 1):
|
|
796
|
-
|
|
855
|
+
parsed_table[i] = item
|
|
797
856
|
|
|
798
|
-
return
|
|
857
|
+
return parsed_table if parsed_table else []
|
|
799
858
|
|
|
800
|
-
def _parse_number(self,
|
|
859
|
+
def _parse_number(self, number_context: LuaParser.NumberContext) -> float:
|
|
801
860
|
"""Parse Lua number to Python number."""
|
|
802
|
-
|
|
861
|
+
number_text = number_context.getText()
|
|
803
862
|
|
|
804
863
|
# Try integer first
|
|
805
864
|
try:
|
|
806
|
-
return int(
|
|
865
|
+
return int(number_text)
|
|
807
866
|
except ValueError:
|
|
808
867
|
pass
|
|
809
868
|
|
|
810
869
|
# Try float
|
|
811
870
|
try:
|
|
812
|
-
return float(
|
|
871
|
+
return float(number_text)
|
|
813
872
|
except ValueError:
|
|
814
873
|
pass
|
|
815
874
|
|
|
816
875
|
# Try hex
|
|
817
|
-
if
|
|
876
|
+
if number_text.startswith("0x") or number_text.startswith("0X"):
|
|
818
877
|
try:
|
|
819
|
-
return int(
|
|
878
|
+
return int(number_text, 16)
|
|
820
879
|
except ValueError:
|
|
821
880
|
pass
|
|
822
881
|
|