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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.0.dist-info/METADATA +1809 -0
- tactus-0.31.0.dist-info/RECORD +160 -0
- tactus-0.31.0.dist-info/WHEEL +4 -0
- tactus-0.31.0.dist-info/entry_points.txt +2 -0
- tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic visitor for Tactus DSL.
|
|
3
|
+
|
|
4
|
+
Walks the ANTLR parse tree and recognizes DSL patterns,
|
|
5
|
+
extracting declarations without executing code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from .generated.LuaParser import LuaParser
|
|
12
|
+
from .generated.LuaParserVisitor import LuaParserVisitor
|
|
13
|
+
from tactus.core.registry import RegistryBuilder, ValidationMessage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TactusDSLVisitor(LuaParserVisitor):
|
|
20
|
+
"""
|
|
21
|
+
Walks ANTLR parse tree and recognizes DSL patterns.
|
|
22
|
+
Does NOT execute code - only analyzes structure.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
DSL_FUNCTIONS = {
|
|
26
|
+
"name",
|
|
27
|
+
"version",
|
|
28
|
+
"description",
|
|
29
|
+
"Agent", # CamelCase
|
|
30
|
+
"Model", # CamelCase
|
|
31
|
+
"Procedure", # CamelCase
|
|
32
|
+
"Prompt", # CamelCase
|
|
33
|
+
"Hitl", # CamelCase
|
|
34
|
+
"Specification", # CamelCase
|
|
35
|
+
"Specifications", # CamelCase - Gherkin BDD specs
|
|
36
|
+
"Step", # CamelCase - Custom step definitions
|
|
37
|
+
"Evaluation", # CamelCase - Evaluation configuration
|
|
38
|
+
"Evaluations", # CamelCase - Pydantic Evals configuration
|
|
39
|
+
"default_provider",
|
|
40
|
+
"default_model",
|
|
41
|
+
"return_prompt",
|
|
42
|
+
"error_prompt",
|
|
43
|
+
"status_prompt",
|
|
44
|
+
"async",
|
|
45
|
+
"max_depth",
|
|
46
|
+
"max_turns",
|
|
47
|
+
"Tool", # CamelCase - Lua-defined tools
|
|
48
|
+
"Toolset", # CamelCase - Added for toolsets
|
|
49
|
+
"input", # lowercase - top-level input schema for script mode
|
|
50
|
+
"output", # lowercase - top-level output schema for script mode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.builder = RegistryBuilder()
|
|
55
|
+
self.errors = []
|
|
56
|
+
self.warnings = []
|
|
57
|
+
self.current_line = 0
|
|
58
|
+
self.current_col = 0
|
|
59
|
+
self.in_function_body = False # Track if we're inside a function body
|
|
60
|
+
|
|
61
|
+
def visitFunctiondef(self, ctx):
|
|
62
|
+
"""Track when entering/exiting function definitions."""
|
|
63
|
+
# Set flag when entering function body
|
|
64
|
+
old_in_function = self.in_function_body
|
|
65
|
+
self.in_function_body = True
|
|
66
|
+
try:
|
|
67
|
+
result = super().visitChildren(ctx)
|
|
68
|
+
finally:
|
|
69
|
+
# Restore previous state when exiting
|
|
70
|
+
self.in_function_body = old_in_function
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def visitStat(self, ctx: LuaParser.StatContext):
|
|
74
|
+
"""Handle statement nodes including assignments."""
|
|
75
|
+
# Check if this is an assignment statement
|
|
76
|
+
if ctx.varlist() and ctx.explist():
|
|
77
|
+
# This is an assignment: varlist '=' explist
|
|
78
|
+
varlist = ctx.varlist()
|
|
79
|
+
explist = ctx.explist()
|
|
80
|
+
|
|
81
|
+
# Get the variable name
|
|
82
|
+
if varlist.var() and len(varlist.var()) > 0:
|
|
83
|
+
var = varlist.var()[0]
|
|
84
|
+
if var.NAME():
|
|
85
|
+
var_name = var.NAME().getText()
|
|
86
|
+
|
|
87
|
+
# Check if this is a DSL setting assignment
|
|
88
|
+
if var_name in [
|
|
89
|
+
"default_provider",
|
|
90
|
+
"default_model",
|
|
91
|
+
"return_prompt",
|
|
92
|
+
"error_prompt",
|
|
93
|
+
"status_prompt",
|
|
94
|
+
"async",
|
|
95
|
+
"max_depth",
|
|
96
|
+
"max_turns",
|
|
97
|
+
]:
|
|
98
|
+
# Get the value from explist
|
|
99
|
+
if explist.exp() and len(explist.exp()) > 0:
|
|
100
|
+
exp = explist.exp()[0]
|
|
101
|
+
value = self._extract_literal_value(exp)
|
|
102
|
+
|
|
103
|
+
# Process the assignment like a function call
|
|
104
|
+
if var_name == "default_provider":
|
|
105
|
+
self.builder.set_default_provider(value)
|
|
106
|
+
elif var_name == "default_model":
|
|
107
|
+
self.builder.set_default_model(value)
|
|
108
|
+
elif var_name == "return_prompt":
|
|
109
|
+
self.builder.set_return_prompt(value)
|
|
110
|
+
elif var_name == "error_prompt":
|
|
111
|
+
self.builder.set_error_prompt(value)
|
|
112
|
+
elif var_name == "status_prompt":
|
|
113
|
+
self.builder.set_status_prompt(value)
|
|
114
|
+
elif var_name == "async":
|
|
115
|
+
self.builder.set_async(value)
|
|
116
|
+
elif var_name == "max_depth":
|
|
117
|
+
self.builder.set_max_depth(value)
|
|
118
|
+
elif var_name == "max_turns":
|
|
119
|
+
self.builder.set_max_turns(value)
|
|
120
|
+
else:
|
|
121
|
+
# Check for assignment-based DSL declarations
|
|
122
|
+
# e.g., greeter = Agent {...}, done = Tool {...}
|
|
123
|
+
if explist.exp() and len(explist.exp()) > 0:
|
|
124
|
+
exp = explist.exp()[0]
|
|
125
|
+
self._check_assignment_based_declaration(var_name, exp)
|
|
126
|
+
|
|
127
|
+
# Continue visiting children
|
|
128
|
+
return self.visitChildren(ctx)
|
|
129
|
+
|
|
130
|
+
def _check_assignment_based_declaration(self, var_name: str, exp):
|
|
131
|
+
"""Check if an assignment is a DSL declaration like 'greeter = Agent {...}'."""
|
|
132
|
+
# Look for prefixexp with functioncall pattern: Agent {...}
|
|
133
|
+
if exp.prefixexp():
|
|
134
|
+
prefixexp = exp.prefixexp()
|
|
135
|
+
if prefixexp.functioncall():
|
|
136
|
+
func_call = prefixexp.functioncall()
|
|
137
|
+
func_name = self._extract_function_name(func_call)
|
|
138
|
+
|
|
139
|
+
# Check if this is a chained method call (e.g., Agent('name').turn())
|
|
140
|
+
# Chained calls have structure: func_name args . method_name args
|
|
141
|
+
# Simple declarations have: func_name args or func_name table
|
|
142
|
+
# If there are more than 2 children, it's a chained call, not a declaration
|
|
143
|
+
is_chained_call = func_call.getChildCount() > 2
|
|
144
|
+
|
|
145
|
+
if func_name == "Agent" and not is_chained_call:
|
|
146
|
+
# Extract config from Agent {...}
|
|
147
|
+
config = self._extract_single_table_arg(func_call)
|
|
148
|
+
# Filter out None values from tools list (variable refs can't be resolved)
|
|
149
|
+
if config and "tools" in config:
|
|
150
|
+
tools = config["tools"]
|
|
151
|
+
if isinstance(tools, list):
|
|
152
|
+
config["tools"] = [t for t in tools if t is not None]
|
|
153
|
+
self.builder.register_agent(var_name, config if config else {}, None)
|
|
154
|
+
elif func_name == "Tool":
|
|
155
|
+
# Extract config from Tool {...}
|
|
156
|
+
config = self._extract_single_table_arg(func_call)
|
|
157
|
+
if (
|
|
158
|
+
config
|
|
159
|
+
and isinstance(config, dict)
|
|
160
|
+
and isinstance(config.get("name"), str)
|
|
161
|
+
and config.get("name") != var_name
|
|
162
|
+
):
|
|
163
|
+
self.errors.append(
|
|
164
|
+
ValidationMessage(
|
|
165
|
+
level="error",
|
|
166
|
+
message=(
|
|
167
|
+
f"Tool name mismatch: '{var_name} = Tool {{ name = \"{config.get('name')}\" }}'. "
|
|
168
|
+
f"Remove the 'name' field or set it to '{var_name}'."
|
|
169
|
+
),
|
|
170
|
+
location=(self.current_line, self.current_col),
|
|
171
|
+
declaration="Tool",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
self.builder.register_tool(var_name, config if config else {}, None)
|
|
175
|
+
elif func_name == "Toolset":
|
|
176
|
+
# Extract config from Toolset {...}
|
|
177
|
+
config = self._extract_single_table_arg(func_call)
|
|
178
|
+
self.builder.register_toolset(var_name, config if config else {})
|
|
179
|
+
elif func_name == "Procedure":
|
|
180
|
+
# New assignment syntax: main = Procedure { function(input) ... }
|
|
181
|
+
# Register as a named procedure
|
|
182
|
+
self.builder.register_named_procedure(
|
|
183
|
+
var_name,
|
|
184
|
+
None, # Function not available during validation
|
|
185
|
+
{}, # Input schema will be extracted from top-level input {}
|
|
186
|
+
{}, # Output schema will be extracted from top-level output {}
|
|
187
|
+
{}, # State schema
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _extract_single_table_arg(self, func_call) -> dict:
|
|
191
|
+
"""Extract a single table argument from a function call like Agent {...}."""
|
|
192
|
+
args_list = func_call.args()
|
|
193
|
+
if not args_list:
|
|
194
|
+
return {}
|
|
195
|
+
|
|
196
|
+
# Process first args entry only
|
|
197
|
+
if len(args_list) > 0:
|
|
198
|
+
args_ctx = args_list[0]
|
|
199
|
+
if args_ctx.tableconstructor():
|
|
200
|
+
return self._parse_table_constructor(args_ctx.tableconstructor())
|
|
201
|
+
|
|
202
|
+
return {}
|
|
203
|
+
|
|
204
|
+
def visitFunctioncall(self, ctx: LuaParser.FunctioncallContext):
|
|
205
|
+
"""Recognize and process DSL function calls."""
|
|
206
|
+
try:
|
|
207
|
+
# Extract line/column for error reporting
|
|
208
|
+
if ctx.start:
|
|
209
|
+
self.current_line = ctx.start.line
|
|
210
|
+
self.current_col = ctx.start.column
|
|
211
|
+
|
|
212
|
+
# Check for deprecated method calls like .turn() or .run()
|
|
213
|
+
self._check_deprecated_method_calls(ctx)
|
|
214
|
+
|
|
215
|
+
func_name = self._extract_function_name(ctx)
|
|
216
|
+
|
|
217
|
+
# Check if this is a method call (e.g., Tool.called()) vs a direct call (e.g., Tool())
|
|
218
|
+
# For "Tool.called()", parser extracts "Tool" as func_name but full text is "Tool.called(...)"
|
|
219
|
+
# We want to skip if full_text shows it's actually calling a method ON Tool, not Tool itself
|
|
220
|
+
full_text = ctx.getText()
|
|
221
|
+
is_method_call = False
|
|
222
|
+
if func_name:
|
|
223
|
+
# If the text is "Tool.called(...)" and func_name is "Tool",
|
|
224
|
+
# then it's actually calling .called() method on Tool, not calling Tool()
|
|
225
|
+
# Check: does full_text have func_name followed by a dot/colon (not by opening paren)?
|
|
226
|
+
# Pattern: func_name followed by . or : means it's accessing a method/property
|
|
227
|
+
import re
|
|
228
|
+
|
|
229
|
+
# Match: funcName followed by . or : (not by opening paren directly)
|
|
230
|
+
method_access_pattern = re.escape(func_name) + r"[.:]"
|
|
231
|
+
if re.search(method_access_pattern, full_text):
|
|
232
|
+
is_method_call = True
|
|
233
|
+
|
|
234
|
+
if func_name in self.DSL_FUNCTIONS and not is_method_call:
|
|
235
|
+
# Process the DSL call (but skip method calls like Tool.called())
|
|
236
|
+
try:
|
|
237
|
+
self._process_dsl_call(func_name, ctx)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.errors.append(
|
|
240
|
+
ValidationMessage(
|
|
241
|
+
level="error",
|
|
242
|
+
message=f"Error processing {func_name}: {e}",
|
|
243
|
+
location=(self.current_line, self.current_col),
|
|
244
|
+
declaration=func_name,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.debug(f"Error in visitFunctioncall: {e}")
|
|
249
|
+
|
|
250
|
+
return self.visitChildren(ctx)
|
|
251
|
+
|
|
252
|
+
def _check_deprecated_method_calls(self, ctx: LuaParser.FunctioncallContext):
|
|
253
|
+
"""Check for deprecated method calls like .turn() or .run()."""
|
|
254
|
+
# Method calls have the form: varOrExp nameAndArgs+
|
|
255
|
+
# The nameAndArgs contains ':' NAME args for method calls
|
|
256
|
+
# We need to check if any nameAndArgs contains 'turn' or 'run'
|
|
257
|
+
|
|
258
|
+
# Get the full text of the function call
|
|
259
|
+
full_text = ctx.getText()
|
|
260
|
+
|
|
261
|
+
# Check for .turn() pattern (method call with dot notation)
|
|
262
|
+
if ".turn(" in full_text or ":turn(" in full_text:
|
|
263
|
+
self.errors.append(
|
|
264
|
+
ValidationMessage(
|
|
265
|
+
level="error",
|
|
266
|
+
message='The .turn() method is deprecated. Use callable syntax instead: agent() or agent({message = "..."})',
|
|
267
|
+
location=(self.current_line, self.current_col),
|
|
268
|
+
declaration="Agent.turn",
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Check for .run() pattern on agents
|
|
273
|
+
if ".run(" in full_text or ":run(" in full_text:
|
|
274
|
+
# Try to determine if this is an agent call (not procedure or other types)
|
|
275
|
+
# If the text contains "Agent(" it's likely an agent method
|
|
276
|
+
if "Agent(" in full_text or ctx.getText().startswith("agent"):
|
|
277
|
+
self.errors.append(
|
|
278
|
+
ValidationMessage(
|
|
279
|
+
level="error",
|
|
280
|
+
message='The .run() method on agents is deprecated. Use callable syntax instead: agent() or agent({message = "..."})',
|
|
281
|
+
location=(self.current_line, self.current_col),
|
|
282
|
+
declaration="Agent.run",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _extract_literal_value(self, exp):
|
|
287
|
+
"""Extract a literal value from an expression node."""
|
|
288
|
+
if not exp:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
# Check for string literals
|
|
292
|
+
if exp.string():
|
|
293
|
+
string_ctx = exp.string()
|
|
294
|
+
# Extract the string value (remove quotes)
|
|
295
|
+
if string_ctx.NORMALSTRING():
|
|
296
|
+
text = string_ctx.NORMALSTRING().getText()
|
|
297
|
+
# Remove surrounding quotes
|
|
298
|
+
if text.startswith('"') and text.endswith('"'):
|
|
299
|
+
return text[1:-1]
|
|
300
|
+
elif text.startswith("'") and text.endswith("'"):
|
|
301
|
+
return text[1:-1]
|
|
302
|
+
elif string_ctx.CHARSTRING():
|
|
303
|
+
text = string_ctx.CHARSTRING().getText()
|
|
304
|
+
# Remove surrounding quotes
|
|
305
|
+
if text.startswith('"') and text.endswith('"'):
|
|
306
|
+
return text[1:-1]
|
|
307
|
+
elif text.startswith("'") and text.endswith("'"):
|
|
308
|
+
return text[1:-1]
|
|
309
|
+
|
|
310
|
+
# Check for number literals
|
|
311
|
+
if exp.number():
|
|
312
|
+
number_ctx = exp.number()
|
|
313
|
+
if number_ctx.INT():
|
|
314
|
+
return int(number_ctx.INT().getText())
|
|
315
|
+
elif number_ctx.FLOAT():
|
|
316
|
+
return float(number_ctx.FLOAT().getText())
|
|
317
|
+
|
|
318
|
+
# Check for boolean literals
|
|
319
|
+
if exp.getText() == "true":
|
|
320
|
+
return True
|
|
321
|
+
elif exp.getText() == "false":
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
# Check for nil
|
|
325
|
+
if exp.getText() == "nil":
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
# Default to the text representation
|
|
329
|
+
return exp.getText()
|
|
330
|
+
|
|
331
|
+
def _extract_function_name(self, ctx: LuaParser.FunctioncallContext) -> Optional[str]:
|
|
332
|
+
"""Extract function name from parse tree."""
|
|
333
|
+
# The function name is the first child of functioncall
|
|
334
|
+
# Look for a terminal node with text
|
|
335
|
+
for i in range(ctx.getChildCount()):
|
|
336
|
+
child = ctx.getChild(i)
|
|
337
|
+
if hasattr(child, "symbol"):
|
|
338
|
+
# It's a terminal node
|
|
339
|
+
text = child.getText()
|
|
340
|
+
if text and text.isidentifier():
|
|
341
|
+
return text
|
|
342
|
+
|
|
343
|
+
# Fallback: try varOrExp approach
|
|
344
|
+
if ctx.varOrExp():
|
|
345
|
+
var_or_exp = ctx.varOrExp()
|
|
346
|
+
# varOrExp: var | '(' exp ')'
|
|
347
|
+
if var_or_exp.var():
|
|
348
|
+
var_ctx = var_or_exp.var()
|
|
349
|
+
# var: (NAME | '(' exp ')' varSuffix) varSuffix*
|
|
350
|
+
if var_ctx.NAME():
|
|
351
|
+
return var_ctx.NAME().getText()
|
|
352
|
+
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def _process_dsl_call(self, func_name: str, ctx: LuaParser.FunctioncallContext):
|
|
356
|
+
"""Extract arguments and register declaration."""
|
|
357
|
+
args = self._extract_arguments(ctx)
|
|
358
|
+
|
|
359
|
+
if func_name == "name":
|
|
360
|
+
if args and len(args) >= 1:
|
|
361
|
+
self.builder.set_name(args[0])
|
|
362
|
+
elif func_name == "version":
|
|
363
|
+
if args and len(args) >= 1:
|
|
364
|
+
self.builder.set_version(args[0])
|
|
365
|
+
elif func_name == "Agent": # CamelCase only
|
|
366
|
+
# Skip Agent calls inside function bodies - they're runtime lookups, not declarations
|
|
367
|
+
if self.in_function_body:
|
|
368
|
+
return self.visitChildren(ctx)
|
|
369
|
+
|
|
370
|
+
if args and len(args) >= 1: # Support curried syntax with just name
|
|
371
|
+
agent_name = args[0]
|
|
372
|
+
# Check if this is a declaration (has config) or a lookup (just name)
|
|
373
|
+
if len(args) >= 2 and isinstance(args[1], dict):
|
|
374
|
+
# DEPRECATED: Curried syntax Agent "name" { config }
|
|
375
|
+
# Raise validation error
|
|
376
|
+
self.errors.append(
|
|
377
|
+
ValidationMessage(
|
|
378
|
+
level="error",
|
|
379
|
+
message=f'Curried syntax Agent "{agent_name}" {{...}} is deprecated. Use assignment syntax: {agent_name} = Agent {{...}}',
|
|
380
|
+
location=(self.current_line, self.current_col),
|
|
381
|
+
declaration="Agent",
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
elif len(args) == 1 and isinstance(agent_name, str):
|
|
385
|
+
# DEPRECATED: Agent("name") lookup or curried declaration
|
|
386
|
+
# This is now invalid - users should use variable references
|
|
387
|
+
self.errors.append(
|
|
388
|
+
ValidationMessage(
|
|
389
|
+
level="error",
|
|
390
|
+
message=f'Agent("{agent_name}") lookup syntax is deprecated. Declare the agent with assignment: {agent_name} = Agent {{...}}, then use {agent_name}() to call it.',
|
|
391
|
+
location=(self.current_line, self.current_col),
|
|
392
|
+
declaration="Agent",
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
elif func_name == "Model": # CamelCase only
|
|
396
|
+
if args and len(args) >= 1:
|
|
397
|
+
# Check if this is assignment syntax (single dict arg) or curried syntax (name + dict)
|
|
398
|
+
if len(args) == 1 and isinstance(args[0], dict):
|
|
399
|
+
# Assignment syntax: my_model = Model {config}
|
|
400
|
+
# Generate a temp name for validation
|
|
401
|
+
import uuid
|
|
402
|
+
|
|
403
|
+
temp_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
|
|
404
|
+
self.builder.register_model(temp_name, args[0])
|
|
405
|
+
elif len(args) >= 2 and isinstance(args[1], dict):
|
|
406
|
+
# Curried syntax: Model "name" {config}
|
|
407
|
+
config = args[1]
|
|
408
|
+
self.builder.register_model(args[0], config)
|
|
409
|
+
elif isinstance(args[0], str):
|
|
410
|
+
# Just a name, register with empty config
|
|
411
|
+
self.builder.register_model(args[0], {})
|
|
412
|
+
elif func_name == "Procedure": # CamelCase only
|
|
413
|
+
# Supports multiple syntax variants:
|
|
414
|
+
# 1. Unnamed (new): Procedure { config with function }
|
|
415
|
+
# 2. Named (curried): Procedure "name" { config }
|
|
416
|
+
# 3. Named (old): procedure("name", {config}, function)
|
|
417
|
+
# Note: args may contain None for unparseable expressions (like functions)
|
|
418
|
+
if args and len(args) >= 1:
|
|
419
|
+
# Check if first arg is a table (unnamed procedure syntax)
|
|
420
|
+
# Tables are parsed as dict if they have named fields, or list if only positional
|
|
421
|
+
if isinstance(args[0], dict):
|
|
422
|
+
# Unnamed syntax: Procedure {...} with named fields
|
|
423
|
+
# e.g., Procedure { output = {...}, function(input) ... end }
|
|
424
|
+
proc_name = "main"
|
|
425
|
+
config = args[0]
|
|
426
|
+
elif isinstance(args[0], list):
|
|
427
|
+
# Unnamed syntax: Procedure {...} with only function (no named fields)
|
|
428
|
+
# e.g., Procedure { function(input) ... end }
|
|
429
|
+
# The list contains [None] for the unparseable function
|
|
430
|
+
proc_name = "main"
|
|
431
|
+
config = {} # No extractable config from function-only table
|
|
432
|
+
elif isinstance(args[0], str):
|
|
433
|
+
# Named syntax: Procedure "name" {...}
|
|
434
|
+
proc_name = args[0]
|
|
435
|
+
config = args[1] if len(args) >= 2 and isinstance(args[1], dict) else None
|
|
436
|
+
else:
|
|
437
|
+
# Invalid syntax
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Register that this named procedure exists (validation needs to know about 'main')
|
|
441
|
+
# We use a stub/placeholder since the actual function will be registered at runtime
|
|
442
|
+
self.builder.register_named_procedure(
|
|
443
|
+
proc_name,
|
|
444
|
+
None, # Function not available during validation
|
|
445
|
+
{}, # Input schema extracted below
|
|
446
|
+
{}, # Output schema extracted below
|
|
447
|
+
{}, # State schema extracted below
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Extract schemas from config if available
|
|
451
|
+
if config is not None and isinstance(config, dict):
|
|
452
|
+
# Extract inline input schema
|
|
453
|
+
if "input" in config and isinstance(config["input"], dict):
|
|
454
|
+
self.builder.register_input_schema(config["input"])
|
|
455
|
+
|
|
456
|
+
# Extract inline output schema
|
|
457
|
+
if "output" in config and isinstance(config["output"], dict):
|
|
458
|
+
self.builder.register_output_schema(config["output"])
|
|
459
|
+
|
|
460
|
+
# Extract inline state schema
|
|
461
|
+
if "state" in config and isinstance(config["state"], dict):
|
|
462
|
+
self.builder.register_state_schema(config["state"])
|
|
463
|
+
elif func_name == "Prompt": # CamelCase
|
|
464
|
+
if args and len(args) >= 2:
|
|
465
|
+
self.builder.register_prompt(args[0], args[1])
|
|
466
|
+
elif func_name == "Hitl": # CamelCase
|
|
467
|
+
if args and len(args) >= 2:
|
|
468
|
+
self.builder.register_hitl(args[0], args[1] if isinstance(args[1], dict) else {})
|
|
469
|
+
elif func_name == "Specification": # CamelCase
|
|
470
|
+
# Either:
|
|
471
|
+
# - Specification([[ Gherkin text ]]) (alias for Specifications)
|
|
472
|
+
# - Specification("name", { ... }) (structured form)
|
|
473
|
+
if args and len(args) == 1:
|
|
474
|
+
self.builder.register_specifications(args[0])
|
|
475
|
+
elif args and len(args) >= 2:
|
|
476
|
+
self.builder.register_specification(
|
|
477
|
+
args[0], args[1] if isinstance(args[1], list) else []
|
|
478
|
+
)
|
|
479
|
+
elif func_name == "Specifications": # CamelCase
|
|
480
|
+
# Specifications([[ Gherkin text ]]) (plural form; singular is Specification([[...]]))
|
|
481
|
+
if args and len(args) >= 1:
|
|
482
|
+
self.builder.register_specifications(args[0])
|
|
483
|
+
elif func_name == "Step": # CamelCase
|
|
484
|
+
# Step("step text", function() ... end)
|
|
485
|
+
if args and len(args) >= 2:
|
|
486
|
+
self.builder.register_custom_step(args[0], args[1])
|
|
487
|
+
elif func_name == "Evaluation": # CamelCase
|
|
488
|
+
# Either:
|
|
489
|
+
# - Evaluation({ runs = 10, parallel = true }) (simple config)
|
|
490
|
+
# - Evaluation({ dataset = {...}, evaluators = {...}, ... }) (alias for Evaluations)
|
|
491
|
+
if args and len(args) >= 1 and isinstance(args[0], dict):
|
|
492
|
+
cfg = args[0]
|
|
493
|
+
if any(k in cfg for k in ("dataset", "dataset_file", "evaluators", "thresholds")):
|
|
494
|
+
self.builder.register_evaluations(cfg)
|
|
495
|
+
else:
|
|
496
|
+
self.builder.set_evaluation_config(cfg)
|
|
497
|
+
elif args and len(args) >= 1:
|
|
498
|
+
self.builder.set_evaluation_config({})
|
|
499
|
+
elif func_name == "Evaluations": # CamelCase
|
|
500
|
+
# Evaluation(s)({ dataset = {...}, evaluators = {...} })
|
|
501
|
+
if args and len(args) >= 1:
|
|
502
|
+
self.builder.register_evaluations(args[0] if isinstance(args[0], dict) else {})
|
|
503
|
+
elif func_name == "default_provider":
|
|
504
|
+
if args and len(args) >= 1:
|
|
505
|
+
self.builder.set_default_provider(args[0])
|
|
506
|
+
elif func_name == "default_model":
|
|
507
|
+
if args and len(args) >= 1:
|
|
508
|
+
self.builder.set_default_model(args[0])
|
|
509
|
+
elif func_name == "return_prompt":
|
|
510
|
+
if args and len(args) >= 1:
|
|
511
|
+
self.builder.set_return_prompt(args[0])
|
|
512
|
+
elif func_name == "error_prompt":
|
|
513
|
+
if args and len(args) >= 1:
|
|
514
|
+
self.builder.set_error_prompt(args[0])
|
|
515
|
+
elif func_name == "status_prompt":
|
|
516
|
+
if args and len(args) >= 1:
|
|
517
|
+
self.builder.set_status_prompt(args[0])
|
|
518
|
+
elif func_name == "async":
|
|
519
|
+
if args and len(args) >= 1:
|
|
520
|
+
self.builder.set_async(args[0])
|
|
521
|
+
elif func_name == "max_depth":
|
|
522
|
+
if args and len(args) >= 1:
|
|
523
|
+
self.builder.set_max_depth(args[0])
|
|
524
|
+
elif func_name == "max_turns":
|
|
525
|
+
if args and len(args) >= 1:
|
|
526
|
+
self.builder.set_max_turns(args[0])
|
|
527
|
+
elif func_name == "input":
|
|
528
|
+
# Top-level input schema for script mode: input { field1 = ..., field2 = ... }
|
|
529
|
+
if args and len(args) >= 1 and isinstance(args[0], dict):
|
|
530
|
+
self.builder.register_top_level_input(args[0])
|
|
531
|
+
elif func_name == "output":
|
|
532
|
+
# Top-level output schema for script mode: output { field1 = ..., field2 = ... }
|
|
533
|
+
if args and len(args) >= 1 and isinstance(args[0], dict):
|
|
534
|
+
self.builder.register_top_level_output(args[0])
|
|
535
|
+
elif func_name == "Tool": # CamelCase only
|
|
536
|
+
# Curried syntax (Tool "name" {...} / Tool("name", ...)) is not supported.
|
|
537
|
+
# Use assignment syntax: my_tool = Tool { ... }.
|
|
538
|
+
if args and len(args) >= 1 and isinstance(args[0], str):
|
|
539
|
+
tool_name = args[0]
|
|
540
|
+
self.errors.append(
|
|
541
|
+
ValidationMessage(
|
|
542
|
+
level="error",
|
|
543
|
+
message=(
|
|
544
|
+
f'Curried Tool syntax is not supported: Tool "{tool_name}" {{...}}. '
|
|
545
|
+
f"Use assignment syntax: {tool_name} = Tool {{...}}."
|
|
546
|
+
),
|
|
547
|
+
location=(self.current_line, self.current_col),
|
|
548
|
+
declaration="Tool",
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
elif func_name == "Toolset": # CamelCase only
|
|
552
|
+
# Toolset("name", {config})
|
|
553
|
+
# or new curried syntax: Toolset "name" { config }
|
|
554
|
+
if args and len(args) >= 1: # Support curried syntax
|
|
555
|
+
# First arg must be name (string)
|
|
556
|
+
if isinstance(args[0], str):
|
|
557
|
+
toolset_name = args[0]
|
|
558
|
+
config = args[1] if len(args) >= 2 and isinstance(args[1], dict) else {}
|
|
559
|
+
# Register the toolset (validation only, no runtime impl yet)
|
|
560
|
+
self.builder.register_toolset(toolset_name, config)
|
|
561
|
+
|
|
562
|
+
def _extract_arguments(self, ctx: LuaParser.FunctioncallContext) -> list:
|
|
563
|
+
"""Extract function arguments from parse tree.
|
|
564
|
+
|
|
565
|
+
Returns a list where:
|
|
566
|
+
- Parseable expressions are included as Python values
|
|
567
|
+
- Unparseable expressions (like functions) are included as None placeholders
|
|
568
|
+
This allows checking total argument count for validation.
|
|
569
|
+
"""
|
|
570
|
+
args = []
|
|
571
|
+
|
|
572
|
+
# functioncall has args() children
|
|
573
|
+
# args: '(' explist? ')' | tableconstructor | LiteralString
|
|
574
|
+
|
|
575
|
+
args_list = ctx.args()
|
|
576
|
+
if not args_list:
|
|
577
|
+
return args
|
|
578
|
+
|
|
579
|
+
# Check if this is a method call chain by looking for '.' or ':' between args
|
|
580
|
+
# For Agent("name").turn({...}), we should only extract "name"
|
|
581
|
+
# For Procedure "name" {...}, we should extract both "name" and {...}
|
|
582
|
+
is_method_chain = False
|
|
583
|
+
if len(args_list) > 1:
|
|
584
|
+
# Check if there's a method access between the first two args
|
|
585
|
+
# Method chains have pattern: func(arg1).method(arg2)
|
|
586
|
+
# Shorthand has pattern: func arg1 arg2
|
|
587
|
+
|
|
588
|
+
# Look at the children of the functioncall context to see if there's
|
|
589
|
+
# a '.' or ':' token between the first and second args
|
|
590
|
+
found_first_args = False
|
|
591
|
+
for i in range(ctx.getChildCount()):
|
|
592
|
+
child = ctx.getChild(i)
|
|
593
|
+
# Check if this is the first args
|
|
594
|
+
if child == args_list[0]:
|
|
595
|
+
found_first_args = True
|
|
596
|
+
elif found_first_args and child == args_list[1]:
|
|
597
|
+
# We've reached the second args without finding . or :
|
|
598
|
+
# So this is NOT a method chain
|
|
599
|
+
break
|
|
600
|
+
elif found_first_args and hasattr(child, "symbol"):
|
|
601
|
+
# Check if this is a . or : token
|
|
602
|
+
token_text = child.getText()
|
|
603
|
+
if token_text in [".", ":"]:
|
|
604
|
+
is_method_chain = True
|
|
605
|
+
break
|
|
606
|
+
|
|
607
|
+
# Process arguments
|
|
608
|
+
if is_method_chain:
|
|
609
|
+
# Only process first args for method chains like Agent("name").turn(...)
|
|
610
|
+
args_to_process = [args_list[0]]
|
|
611
|
+
else:
|
|
612
|
+
# Process all args for shorthand syntax like Procedure "name" {...}
|
|
613
|
+
args_to_process = args_list
|
|
614
|
+
|
|
615
|
+
for args_ctx in args_to_process:
|
|
616
|
+
# Check for different argument types
|
|
617
|
+
if args_ctx.explist():
|
|
618
|
+
# Regular function call with expression list
|
|
619
|
+
explist = args_ctx.explist()
|
|
620
|
+
for exp in explist.exp():
|
|
621
|
+
value = self._parse_expression(exp)
|
|
622
|
+
# Include None placeholders to preserve argument count
|
|
623
|
+
args.append(value)
|
|
624
|
+
elif args_ctx.tableconstructor():
|
|
625
|
+
# Table constructor argument
|
|
626
|
+
table = self._parse_table_constructor(args_ctx.tableconstructor())
|
|
627
|
+
args.append(table)
|
|
628
|
+
elif args_ctx.string():
|
|
629
|
+
# String literal argument
|
|
630
|
+
string_val = self._parse_string(args_ctx.string())
|
|
631
|
+
args.append(string_val)
|
|
632
|
+
|
|
633
|
+
return args
|
|
634
|
+
|
|
635
|
+
def _parse_expression(self, ctx: LuaParser.ExpContext) -> Any:
|
|
636
|
+
"""Parse an expression to a Python value."""
|
|
637
|
+
if not ctx:
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
# Detect field.<type>{...} builder syntax so we can preserve schema info
|
|
641
|
+
prefix = ctx.prefixexp()
|
|
642
|
+
if prefix and prefix.functioncall():
|
|
643
|
+
func_ctx = prefix.functioncall()
|
|
644
|
+
name_tokens = [t.getText() for t in func_ctx.NAME()]
|
|
645
|
+
|
|
646
|
+
# field.string{required = true, ...}
|
|
647
|
+
if len(name_tokens) >= 2 and name_tokens[0] == "field":
|
|
648
|
+
field_type = name_tokens[-1]
|
|
649
|
+
|
|
650
|
+
# Default field definition
|
|
651
|
+
field_def = {"type": field_type, "required": False}
|
|
652
|
+
|
|
653
|
+
# Parse options table if present
|
|
654
|
+
if func_ctx.args():
|
|
655
|
+
# We only expect a single args() entry for the builder
|
|
656
|
+
first_arg = func_ctx.args(0)
|
|
657
|
+
if first_arg.tableconstructor():
|
|
658
|
+
options = self._parse_table_constructor(first_arg.tableconstructor())
|
|
659
|
+
if isinstance(options, dict):
|
|
660
|
+
field_def["required"] = bool(options.get("required", False))
|
|
661
|
+
if "default" in options and not field_def["required"]:
|
|
662
|
+
field_def["default"] = options["default"]
|
|
663
|
+
if "description" in options:
|
|
664
|
+
field_def["description"] = options["description"]
|
|
665
|
+
if "enum" in options:
|
|
666
|
+
field_def["enum"] = options["enum"]
|
|
667
|
+
|
|
668
|
+
return field_def
|
|
669
|
+
|
|
670
|
+
# Check for literals
|
|
671
|
+
if ctx.number():
|
|
672
|
+
return self._parse_number(ctx.number())
|
|
673
|
+
elif ctx.string():
|
|
674
|
+
return self._parse_string(ctx.string())
|
|
675
|
+
elif ctx.NIL():
|
|
676
|
+
return None
|
|
677
|
+
elif ctx.FALSE():
|
|
678
|
+
return False
|
|
679
|
+
elif ctx.TRUE():
|
|
680
|
+
return True
|
|
681
|
+
elif ctx.tableconstructor():
|
|
682
|
+
return self._parse_table_constructor(ctx.tableconstructor())
|
|
683
|
+
|
|
684
|
+
# For other expressions, return None (can't evaluate without execution)
|
|
685
|
+
return None
|
|
686
|
+
|
|
687
|
+
def _parse_string(self, ctx: LuaParser.StringContext) -> str:
|
|
688
|
+
"""Parse string context to Python string."""
|
|
689
|
+
if not ctx:
|
|
690
|
+
return ""
|
|
691
|
+
|
|
692
|
+
# string has NORMALSTRING, CHARSTRING, or LONGSTRING
|
|
693
|
+
if ctx.NORMALSTRING():
|
|
694
|
+
return self._parse_string_token(ctx.NORMALSTRING())
|
|
695
|
+
elif ctx.CHARSTRING():
|
|
696
|
+
return self._parse_string_token(ctx.CHARSTRING())
|
|
697
|
+
elif ctx.LONGSTRING():
|
|
698
|
+
return self._parse_string_token(ctx.LONGSTRING())
|
|
699
|
+
|
|
700
|
+
return ""
|
|
701
|
+
|
|
702
|
+
def _parse_string_token(self, token) -> str:
|
|
703
|
+
"""Parse string token to Python string."""
|
|
704
|
+
text = token.getText()
|
|
705
|
+
|
|
706
|
+
# Handle different Lua string formats
|
|
707
|
+
if text.startswith("[[") and text.endswith("]]"):
|
|
708
|
+
# Long string literal
|
|
709
|
+
return text[2:-2]
|
|
710
|
+
elif text.startswith('"') and text.endswith('"'):
|
|
711
|
+
# Double-quoted string
|
|
712
|
+
content = text[1:-1]
|
|
713
|
+
content = content.replace("\\n", "\n")
|
|
714
|
+
content = content.replace("\\t", "\t")
|
|
715
|
+
content = content.replace('\\"', '"')
|
|
716
|
+
content = content.replace("\\\\", "\\")
|
|
717
|
+
return content
|
|
718
|
+
elif text.startswith("'") and text.endswith("'"):
|
|
719
|
+
# Single-quoted string
|
|
720
|
+
content = text[1:-1]
|
|
721
|
+
content = content.replace("\\n", "\n")
|
|
722
|
+
content = content.replace("\\t", "\t")
|
|
723
|
+
content = content.replace("\\'", "'")
|
|
724
|
+
content = content.replace("\\\\", "\\")
|
|
725
|
+
return content
|
|
726
|
+
|
|
727
|
+
return text
|
|
728
|
+
|
|
729
|
+
def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) -> dict:
|
|
730
|
+
"""Parse Lua table constructor to Python dict."""
|
|
731
|
+
result = {}
|
|
732
|
+
array_items = []
|
|
733
|
+
|
|
734
|
+
if not ctx or not ctx.fieldlist():
|
|
735
|
+
# Empty table
|
|
736
|
+
return [] # Return empty list for empty tables (matches runtime behavior)
|
|
737
|
+
|
|
738
|
+
fieldlist = ctx.fieldlist()
|
|
739
|
+
for field in fieldlist.field():
|
|
740
|
+
# field: '[' exp ']' '=' exp | NAME '=' exp | exp
|
|
741
|
+
if field.NAME():
|
|
742
|
+
# Named field: NAME '=' exp
|
|
743
|
+
key = field.NAME().getText()
|
|
744
|
+
value = self._parse_expression(field.exp(0))
|
|
745
|
+
|
|
746
|
+
# Check for old type syntax in field definitions
|
|
747
|
+
# Only check if this looks like a field definition (has type + required/description)
|
|
748
|
+
if (
|
|
749
|
+
key == "type"
|
|
750
|
+
and isinstance(value, str)
|
|
751
|
+
and value in ["string", "number", "boolean", "integer", "array", "object"]
|
|
752
|
+
):
|
|
753
|
+
# Check if the parent table also has 'required' or 'description' keys
|
|
754
|
+
# which would indicate this is a field definition, not a JSON schema or evaluator config
|
|
755
|
+
parent_text = ctx.getText() if ctx else ""
|
|
756
|
+
# Skip if this is part of JSON schema or evaluator configuration
|
|
757
|
+
if (
|
|
758
|
+
"json_schema" not in parent_text
|
|
759
|
+
and "evaluators" not in parent_text
|
|
760
|
+
and "properties" not in parent_text # JSON schema has 'properties'
|
|
761
|
+
and ("required=" in parent_text or "description=" in parent_text)
|
|
762
|
+
):
|
|
763
|
+
self.errors.append(
|
|
764
|
+
ValidationMessage(
|
|
765
|
+
level="error",
|
|
766
|
+
message=f"Old type syntax detected. Use field.{value}{{}} instead of {{type = '{value}'}}",
|
|
767
|
+
line=field.start.line if field.start else 0,
|
|
768
|
+
column=field.start.column if field.start else 0,
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
result[key] = value
|
|
773
|
+
elif len(field.exp()) == 2:
|
|
774
|
+
# Indexed field: '[' exp ']' '=' exp
|
|
775
|
+
# Skip for now (complex)
|
|
776
|
+
pass
|
|
777
|
+
elif len(field.exp()) == 1:
|
|
778
|
+
# Array element: exp
|
|
779
|
+
value = self._parse_expression(field.exp(0))
|
|
780
|
+
array_items.append(value)
|
|
781
|
+
|
|
782
|
+
# If we only have array items, return as list
|
|
783
|
+
if array_items and not result:
|
|
784
|
+
return array_items
|
|
785
|
+
|
|
786
|
+
# If we have both, prefer dict (shouldn't happen in DSL)
|
|
787
|
+
if array_items:
|
|
788
|
+
# Mixed table - add array items with numeric keys
|
|
789
|
+
for i, item in enumerate(array_items, 1):
|
|
790
|
+
result[i] = item
|
|
791
|
+
|
|
792
|
+
return result if result else []
|
|
793
|
+
|
|
794
|
+
def _parse_number(self, ctx: LuaParser.NumberContext) -> float:
|
|
795
|
+
"""Parse Lua number to Python number."""
|
|
796
|
+
text = ctx.getText()
|
|
797
|
+
|
|
798
|
+
# Try integer first
|
|
799
|
+
try:
|
|
800
|
+
return int(text)
|
|
801
|
+
except ValueError:
|
|
802
|
+
pass
|
|
803
|
+
|
|
804
|
+
# Try float
|
|
805
|
+
try:
|
|
806
|
+
return float(text)
|
|
807
|
+
except ValueError:
|
|
808
|
+
pass
|
|
809
|
+
|
|
810
|
+
# Try hex
|
|
811
|
+
if text.startswith("0x") or text.startswith("0X"):
|
|
812
|
+
try:
|
|
813
|
+
return int(text, 16)
|
|
814
|
+
except ValueError:
|
|
815
|
+
pass
|
|
816
|
+
|
|
817
|
+
return 0
|