tactus 0.37.0__py3-none-any.whl → 0.39.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tactus/__init__.py +1 -1
- tactus/adapters/channels/base.py +2 -0
- tactus/cli/app.py +212 -57
- tactus/core/compaction.py +17 -0
- tactus/core/context_assembler.py +73 -0
- tactus/core/context_models.py +41 -0
- tactus/core/dsl_stubs.py +568 -17
- tactus/core/exceptions.py +8 -0
- tactus/core/execution_context.py +1 -1
- tactus/core/mocking.py +12 -0
- tactus/core/registry.py +142 -0
- tactus/core/retrieval.py +317 -0
- tactus/core/retriever_tasks.py +30 -0
- tactus/core/runtime.py +441 -75
- tactus/dspy/agent.py +143 -82
- tactus/dspy/config.py +16 -0
- tactus/dspy/module.py +12 -1
- tactus/ide/coding_assistant.py +2 -2
- tactus/plugins/__init__.py +3 -0
- tactus/plugins/noaa.py +76 -0
- tactus/primitives/handles.py +79 -7
- tactus/sandbox/config.py +1 -1
- tactus/sandbox/container_runner.py +2 -0
- tactus/sandbox/entrypoint.py +51 -8
- tactus/sandbox/protocol.py +5 -0
- tactus/stdlib/README.md +10 -1
- tactus/stdlib/biblicus/__init__.py +3 -0
- tactus/stdlib/biblicus/text.py +208 -0
- tactus/stdlib/tac/biblicus/text.tac +32 -0
- tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
- tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
- tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
- tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
- tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
- tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
- tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
- tactus/testing/behave_integration.py +2 -0
- tactus/testing/context.py +4 -0
- tactus/validation/semantic_visitor.py +430 -88
- tactus/validation/validator.py +142 -2
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/METADATA +3 -2
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/RECORD +48 -28
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/WHEEL +0 -0
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -29,6 +29,8 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
29
29
|
"Agent", # CamelCase
|
|
30
30
|
"Model", # CamelCase
|
|
31
31
|
"Procedure", # CamelCase
|
|
32
|
+
"Task", # CamelCase
|
|
33
|
+
"IncludeTasks", # CamelCase
|
|
32
34
|
"Prompt", # CamelCase
|
|
33
35
|
"Hitl", # CamelCase
|
|
34
36
|
"Specification", # CamelCase
|
|
@@ -57,6 +59,9 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
57
59
|
self.current_line = 0
|
|
58
60
|
self.current_col = 0
|
|
59
61
|
self.in_function_body = False # Track if we're inside a function body
|
|
62
|
+
self._processed_task_calls: set[int] = set()
|
|
63
|
+
self._retriever_aliases: dict[str, str] = {}
|
|
64
|
+
self._retriever_modules: dict[str, str] = {}
|
|
60
65
|
|
|
61
66
|
def _record_error(
|
|
62
67
|
self,
|
|
@@ -90,6 +95,27 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
90
95
|
self.in_function_body = previous_in_function_body
|
|
91
96
|
return child_visit_result
|
|
92
97
|
|
|
98
|
+
def visitChunk(self, context: LuaParser.ChunkContext):
|
|
99
|
+
"""Enforce IncludeTasks file task-only constraint when used."""
|
|
100
|
+
result = self.visitChildren(context)
|
|
101
|
+
if self.builder.registry.include_tasks:
|
|
102
|
+
# If any non-task declarations exist, error out.
|
|
103
|
+
if (
|
|
104
|
+
self.builder.registry.agents
|
|
105
|
+
or self.builder.registry.toolsets
|
|
106
|
+
or self.builder.registry.lua_tools
|
|
107
|
+
or self.builder.registry.contexts
|
|
108
|
+
or self.builder.registry.corpora
|
|
109
|
+
or self.builder.registry.retrievers
|
|
110
|
+
or self.builder.registry.compactors
|
|
111
|
+
or self.builder.registry.named_procedures
|
|
112
|
+
):
|
|
113
|
+
self._record_error(
|
|
114
|
+
"IncludeTasks files must only contain Task declarations.",
|
|
115
|
+
declaration="IncludeTasks",
|
|
116
|
+
)
|
|
117
|
+
return result
|
|
118
|
+
|
|
93
119
|
def visitStat(self, context: LuaParser.StatContext):
|
|
94
120
|
"""Handle statement nodes including assignments."""
|
|
95
121
|
# Check if this is an assignment statement
|
|
@@ -103,34 +129,25 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
103
129
|
assignment_target_node = variable_list.var()[0]
|
|
104
130
|
if assignment_target_node.NAME():
|
|
105
131
|
assignment_target_name = assignment_target_node.NAME().getText()
|
|
132
|
+
self._track_retriever_alias(assignment_target_name, expression_list)
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
119
|
-
# Get the value from explist
|
|
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)
|
|
123
|
-
# Process the assignment like a function call
|
|
124
|
-
setting_handlers_by_name[assignment_target_name](literal_value)
|
|
125
|
-
else:
|
|
126
|
-
# Check for assignment-based DSL declarations
|
|
127
|
-
# e.g., greeter = Agent {...}, done = Tool {...}
|
|
128
|
-
if expression_list.exp() and len(expression_list.exp()) > 0:
|
|
129
|
-
first_expression = expression_list.exp()[0]
|
|
134
|
+
if not self._apply_setting_assignment(assignment_target_name, expression_list):
|
|
135
|
+
first_expression = self._extract_first_expression(expression_list)
|
|
136
|
+
if first_expression:
|
|
130
137
|
self._check_assignment_based_declaration(
|
|
131
138
|
assignment_target_name, first_expression
|
|
132
139
|
)
|
|
133
140
|
|
|
141
|
+
attname_list = getattr(context, "attnamelist", None)
|
|
142
|
+
exp_list = getattr(context, "explist", None)
|
|
143
|
+
if callable(attname_list) and callable(exp_list):
|
|
144
|
+
attname_nodes = attname_list()
|
|
145
|
+
exp_nodes = exp_list()
|
|
146
|
+
if attname_nodes and exp_nodes:
|
|
147
|
+
name_nodes = attname_nodes.NAME()
|
|
148
|
+
if name_nodes:
|
|
149
|
+
self._track_retriever_alias(name_nodes[0].getText(), exp_nodes)
|
|
150
|
+
|
|
134
151
|
# Continue visiting children
|
|
135
152
|
return self.visitChildren(context)
|
|
136
153
|
|
|
@@ -144,70 +161,162 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
144
161
|
if prefix_expression.functioncall():
|
|
145
162
|
function_call = prefix_expression.functioncall()
|
|
146
163
|
function_name = self._extract_function_name(function_call)
|
|
164
|
+
dotted_name = None
|
|
165
|
+
if function_name not in {
|
|
166
|
+
"Agent",
|
|
167
|
+
"Tool",
|
|
168
|
+
"Toolset",
|
|
169
|
+
"Context",
|
|
170
|
+
"Corpus",
|
|
171
|
+
"Retriever",
|
|
172
|
+
"Compactor",
|
|
173
|
+
}:
|
|
174
|
+
dotted_name = self._extract_dotted_dsl_name(function_call)
|
|
175
|
+
if dotted_name:
|
|
176
|
+
function_name = dotted_name
|
|
177
|
+
else:
|
|
178
|
+
dotted_name = self._extract_dotted_dsl_name(function_call)
|
|
179
|
+
|
|
180
|
+
# Check if this is a chained method call (e.g., Agent('name').turn()).
|
|
181
|
+
# Dotted module calls like vector.Corpus { ... } are not chained.
|
|
182
|
+
get_text = getattr(function_call, "getText", None)
|
|
183
|
+
function_call_text = get_text() if callable(get_text) else ""
|
|
184
|
+
is_chained_method_call = (
|
|
185
|
+
re.search(r"[)}\\]]\\s*\\.", function_call_text) is not None
|
|
186
|
+
)
|
|
147
187
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# If there are more than 2 children, it's a chained call, not a declaration
|
|
152
|
-
is_chained_method_call = function_call.getChildCount() > 2
|
|
153
|
-
|
|
154
|
-
if function_name == "Agent" and not is_chained_method_call:
|
|
155
|
-
# Extract config from Agent {...}
|
|
156
|
-
declaration_config = self._extract_single_table_arg(function_call)
|
|
157
|
-
# Filter out None values from tools list (variable refs can't be resolved)
|
|
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,
|
|
188
|
+
if function_name in {"Corpus", "Retriever"}:
|
|
189
|
+
has_namespace = dotted_name is not None or re.match(
|
|
190
|
+
r"[A-Za-z_][A-Za-z0-9_]*\\.", function_call_text
|
|
168
191
|
)
|
|
169
|
-
|
|
170
|
-
# Extract config from Tool {...}
|
|
171
|
-
declaration_config = self._extract_single_table_arg(function_call)
|
|
172
|
-
if (
|
|
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
|
|
177
|
-
):
|
|
192
|
+
if not has_namespace:
|
|
178
193
|
self._record_error(
|
|
179
194
|
message=(
|
|
180
|
-
f"
|
|
181
|
-
|
|
195
|
+
f"Direct {function_name} declarations are not supported. "
|
|
196
|
+
"Use module.Corpus { ... } instead."
|
|
182
197
|
),
|
|
183
|
-
declaration=
|
|
198
|
+
declaration=function_name,
|
|
184
199
|
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
self.builder.register_named_procedure(
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if function_name == "Agent" and is_chained_method_call:
|
|
203
|
+
return
|
|
204
|
+
handled = self._register_assignment_based_declaration(
|
|
205
|
+
assignment_target_name, function_name, function_call
|
|
206
|
+
)
|
|
207
|
+
if handled:
|
|
208
|
+
return
|
|
209
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
210
|
+
if isinstance(declaration_config, dict) and "corpus" in declaration_config:
|
|
211
|
+
retriever_id = self._resolve_retriever_id_from_call(function_call)
|
|
212
|
+
if retriever_id and "retriever_id" not in declaration_config:
|
|
213
|
+
declaration_config["retriever_id"] = retriever_id
|
|
214
|
+
self.builder.register_retriever(
|
|
201
215
|
assignment_target_name,
|
|
202
|
-
|
|
203
|
-
{}, # Input schema will be extracted from top-level input {}
|
|
204
|
-
{}, # Output schema will be extracted from top-level output {}
|
|
205
|
-
{}, # State schema
|
|
216
|
+
declaration_config,
|
|
206
217
|
)
|
|
207
218
|
|
|
219
|
+
def _apply_setting_assignment(self, assignment_target_name: str, expression_list) -> bool:
|
|
220
|
+
setting_handlers_by_name = {
|
|
221
|
+
"default_provider": self.builder.set_default_provider,
|
|
222
|
+
"default_model": self.builder.set_default_model,
|
|
223
|
+
"return_prompt": self.builder.set_return_prompt,
|
|
224
|
+
"error_prompt": self.builder.set_error_prompt,
|
|
225
|
+
"status_prompt": self.builder.set_status_prompt,
|
|
226
|
+
"async": self.builder.set_async,
|
|
227
|
+
"max_depth": self.builder.set_max_depth,
|
|
228
|
+
"max_turns": self.builder.set_max_turns,
|
|
229
|
+
}
|
|
230
|
+
if assignment_target_name not in setting_handlers_by_name:
|
|
231
|
+
return False
|
|
232
|
+
first_expression = self._extract_first_expression(expression_list)
|
|
233
|
+
if not first_expression:
|
|
234
|
+
return True
|
|
235
|
+
literal_value = self._extract_literal_value(first_expression)
|
|
236
|
+
setting_handlers_by_name[assignment_target_name](literal_value)
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def _extract_first_expression(self, expression_list):
|
|
240
|
+
exp_list = getattr(expression_list, "exp", None)
|
|
241
|
+
if not callable(exp_list):
|
|
242
|
+
return None
|
|
243
|
+
expressions = exp_list()
|
|
244
|
+
if expressions:
|
|
245
|
+
return expressions[0]
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
def _register_assignment_based_declaration(
|
|
249
|
+
self,
|
|
250
|
+
assignment_target_name: str,
|
|
251
|
+
function_name: str,
|
|
252
|
+
function_call,
|
|
253
|
+
) -> bool:
|
|
254
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
255
|
+
normalized_config = declaration_config if declaration_config else {}
|
|
256
|
+
if function_name == "Agent":
|
|
257
|
+
if "tools" in normalized_config:
|
|
258
|
+
tool_name_list = normalized_config["tools"]
|
|
259
|
+
if isinstance(tool_name_list, list):
|
|
260
|
+
normalized_config["tools"] = [
|
|
261
|
+
tool_name for tool_name in tool_name_list if tool_name is not None
|
|
262
|
+
]
|
|
263
|
+
self.builder.register_agent(assignment_target_name, normalized_config, None)
|
|
264
|
+
return True
|
|
265
|
+
if function_name == "Tool":
|
|
266
|
+
if (
|
|
267
|
+
isinstance(normalized_config, dict)
|
|
268
|
+
and isinstance(normalized_config.get("name"), str)
|
|
269
|
+
and normalized_config.get("name") != assignment_target_name
|
|
270
|
+
):
|
|
271
|
+
self._record_error(
|
|
272
|
+
message=(
|
|
273
|
+
f"Tool name mismatch: '{assignment_target_name} = Tool {{ name = \"{normalized_config.get('name')}\" }}'. "
|
|
274
|
+
f"Remove the 'name' field or set it to '{assignment_target_name}'."
|
|
275
|
+
),
|
|
276
|
+
declaration="Tool",
|
|
277
|
+
)
|
|
278
|
+
self.builder.register_tool(assignment_target_name, normalized_config, None)
|
|
279
|
+
return True
|
|
280
|
+
if function_name == "Toolset":
|
|
281
|
+
self.builder.register_toolset(assignment_target_name, normalized_config)
|
|
282
|
+
return True
|
|
283
|
+
if function_name == "Context":
|
|
284
|
+
self.builder.register_context(assignment_target_name, normalized_config)
|
|
285
|
+
return True
|
|
286
|
+
if function_name == "Corpus":
|
|
287
|
+
self.builder.register_corpus(assignment_target_name, normalized_config)
|
|
288
|
+
return True
|
|
289
|
+
if function_name == "Retriever":
|
|
290
|
+
retriever_id = self._resolve_retriever_id_from_call(function_call)
|
|
291
|
+
if retriever_id and "retriever_id" not in normalized_config:
|
|
292
|
+
normalized_config["retriever_id"] = retriever_id
|
|
293
|
+
self.builder.register_retriever(assignment_target_name, normalized_config)
|
|
294
|
+
return True
|
|
295
|
+
if function_name == "Compactor":
|
|
296
|
+
self.builder.register_compactor(assignment_target_name, normalized_config)
|
|
297
|
+
return True
|
|
298
|
+
if function_name == "Procedure":
|
|
299
|
+
self.builder.register_named_procedure(
|
|
300
|
+
assignment_target_name,
|
|
301
|
+
None,
|
|
302
|
+
{},
|
|
303
|
+
{},
|
|
304
|
+
{},
|
|
305
|
+
)
|
|
306
|
+
return True
|
|
307
|
+
if function_name == "Task":
|
|
308
|
+
self._processed_task_calls.add(id(function_call))
|
|
309
|
+
self._register_task_declaration(assignment_target_name, function_call)
|
|
310
|
+
return True
|
|
311
|
+
return False
|
|
312
|
+
|
|
208
313
|
def _extract_single_table_arg(self, function_call) -> dict:
|
|
209
314
|
"""Extract a single table argument from a function call like Agent {...}."""
|
|
210
|
-
|
|
315
|
+
args_method = getattr(function_call, "args", None)
|
|
316
|
+
if args_method is None:
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
argument_list_nodes = args_method()
|
|
211
320
|
if not argument_list_nodes:
|
|
212
321
|
return {}
|
|
213
322
|
|
|
@@ -239,6 +348,14 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
239
348
|
if function_name in self.DSL_FUNCTIONS and not is_method_access_call:
|
|
240
349
|
# Process the DSL call (but skip method calls like Tool.called())
|
|
241
350
|
try:
|
|
351
|
+
if function_name == "Task":
|
|
352
|
+
if id(context) in self._processed_task_calls:
|
|
353
|
+
return None
|
|
354
|
+
self._register_task_declaration(None, context)
|
|
355
|
+
return None
|
|
356
|
+
if function_name == "IncludeTasks":
|
|
357
|
+
self._register_include_tasks(context)
|
|
358
|
+
return None
|
|
242
359
|
self._process_dsl_call(function_name, context)
|
|
243
360
|
except Exception as processing_exception:
|
|
244
361
|
self.errors.append(
|
|
@@ -254,6 +371,122 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
254
371
|
|
|
255
372
|
return self.visitChildren(context)
|
|
256
373
|
|
|
374
|
+
def _register_task_declaration(
|
|
375
|
+
self,
|
|
376
|
+
assignment_name: Optional[str],
|
|
377
|
+
function_call_context: LuaParser.FunctioncallContext,
|
|
378
|
+
parent_task: Optional[str] = None,
|
|
379
|
+
) -> None:
|
|
380
|
+
args = self._extract_arguments(function_call_context)
|
|
381
|
+
task_name = None
|
|
382
|
+
task_config: dict[str, Any] = {}
|
|
383
|
+
|
|
384
|
+
if args:
|
|
385
|
+
if isinstance(args[0], str):
|
|
386
|
+
task_name = args[0]
|
|
387
|
+
elif isinstance(args[0], dict):
|
|
388
|
+
task_config = args[0]
|
|
389
|
+
|
|
390
|
+
if len(args) >= 2 and isinstance(args[1], dict):
|
|
391
|
+
task_config = args[1]
|
|
392
|
+
|
|
393
|
+
if assignment_name and task_name and assignment_name != task_name:
|
|
394
|
+
self._record_error(
|
|
395
|
+
message=(
|
|
396
|
+
f"Task name mismatch: '{assignment_name} = Task \"{task_name}\" {{ ... }}'. "
|
|
397
|
+
f"Remove the string name or set it to '{assignment_name}'."
|
|
398
|
+
),
|
|
399
|
+
declaration="Task",
|
|
400
|
+
)
|
|
401
|
+
task_name = assignment_name
|
|
402
|
+
|
|
403
|
+
if assignment_name and not task_name:
|
|
404
|
+
task_name = assignment_name
|
|
405
|
+
|
|
406
|
+
if not task_name:
|
|
407
|
+
self._record_error("Task name is required.", declaration="Task")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
children = self._extract_nested_tasks(function_call_context)
|
|
411
|
+
for child in children:
|
|
412
|
+
child_name = child["name"]
|
|
413
|
+
self.builder.register_task(child_name, child.get("config", {}), parent=task_name)
|
|
414
|
+
|
|
415
|
+
self.builder.register_task(task_name, task_config, parent=parent_task)
|
|
416
|
+
|
|
417
|
+
def _extract_nested_tasks(
|
|
418
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
419
|
+
) -> list[dict[str, Any]]:
|
|
420
|
+
"""Extract nested Task calls from a Task { ... } table constructor."""
|
|
421
|
+
nested_tasks: list[dict[str, Any]] = []
|
|
422
|
+
argument_list_nodes = function_call_context.args()
|
|
423
|
+
if not argument_list_nodes:
|
|
424
|
+
return nested_tasks
|
|
425
|
+
|
|
426
|
+
for args_ctx in argument_list_nodes:
|
|
427
|
+
table_ctx = args_ctx.tableconstructor()
|
|
428
|
+
if not table_ctx or not table_ctx.fieldlist():
|
|
429
|
+
continue
|
|
430
|
+
for field in table_ctx.fieldlist().field():
|
|
431
|
+
if len(field.exp()) != 1:
|
|
432
|
+
continue
|
|
433
|
+
child_exp = field.exp(0)
|
|
434
|
+
child_call = None
|
|
435
|
+
if hasattr(child_exp, "functioncall") and child_exp.functioncall():
|
|
436
|
+
child_call = child_exp.functioncall()
|
|
437
|
+
elif child_exp.prefixexp() and child_exp.prefixexp().functioncall():
|
|
438
|
+
child_call = child_exp.prefixexp().functioncall()
|
|
439
|
+
if not child_call:
|
|
440
|
+
continue
|
|
441
|
+
child_name = self._extract_function_name(child_call)
|
|
442
|
+
if child_name != "Task":
|
|
443
|
+
continue
|
|
444
|
+
child_args = self._extract_arguments(child_call)
|
|
445
|
+
child_task_name = None
|
|
446
|
+
if field.NAME():
|
|
447
|
+
child_task_name = field.NAME().getText()
|
|
448
|
+
if not child_task_name and child_args:
|
|
449
|
+
child_task_name = child_args[0] if isinstance(child_args[0], str) else None
|
|
450
|
+
child_task_config = {}
|
|
451
|
+
if len(child_args) >= 2 and isinstance(child_args[1], dict):
|
|
452
|
+
child_task_config = child_args[1]
|
|
453
|
+
if field.NAME() and child_args and isinstance(child_args[0], str):
|
|
454
|
+
if field.NAME().getText() != child_args[0]:
|
|
455
|
+
self._record_error(
|
|
456
|
+
message=(
|
|
457
|
+
f"Task name mismatch: '{field.NAME().getText()} = Task \"{child_args[0]}\" {{ ... }}'. "
|
|
458
|
+
f"Remove the string name or set it to '{field.NAME().getText()}'."
|
|
459
|
+
),
|
|
460
|
+
declaration="Task",
|
|
461
|
+
)
|
|
462
|
+
if child_task_name:
|
|
463
|
+
nested_tasks.append(
|
|
464
|
+
{
|
|
465
|
+
"name": child_task_name,
|
|
466
|
+
"config": child_task_config,
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
return nested_tasks
|
|
470
|
+
|
|
471
|
+
def _register_include_tasks(self, context: LuaParser.FunctioncallContext) -> None:
|
|
472
|
+
args = self._extract_arguments(context)
|
|
473
|
+
if not args:
|
|
474
|
+
self._record_error("IncludeTasks requires a path string.", declaration="IncludeTasks")
|
|
475
|
+
return
|
|
476
|
+
if not isinstance(args[0], str):
|
|
477
|
+
self._record_error(
|
|
478
|
+
"IncludeTasks path must be a string literal.",
|
|
479
|
+
declaration="IncludeTasks",
|
|
480
|
+
)
|
|
481
|
+
return
|
|
482
|
+
namespace = None
|
|
483
|
+
if len(args) >= 2:
|
|
484
|
+
if isinstance(args[1], str):
|
|
485
|
+
namespace = args[1]
|
|
486
|
+
elif isinstance(args[1], dict):
|
|
487
|
+
namespace = args[1].get("namespace")
|
|
488
|
+
self.builder.register_include_tasks(args[0], namespace)
|
|
489
|
+
|
|
257
490
|
def _check_deprecated_method_calls(self, ctx: LuaParser.FunctioncallContext):
|
|
258
491
|
"""Check for deprecated method calls like .turn() or .run()."""
|
|
259
492
|
# Method calls have the form: varOrExp nameAndArgs+
|
|
@@ -373,6 +606,112 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
373
606
|
|
|
374
607
|
return None
|
|
375
608
|
|
|
609
|
+
def _extract_dotted_dsl_name(
|
|
610
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
611
|
+
) -> Optional[str]:
|
|
612
|
+
"""Extract DSL names from dotted calls like module.Corpus {...}."""
|
|
613
|
+
get_text = getattr(function_call_context, "getText", None)
|
|
614
|
+
if not callable(get_text):
|
|
615
|
+
return None
|
|
616
|
+
function_call_text = get_text()
|
|
617
|
+
match = re.match(
|
|
618
|
+
r"(?:[A-Za-z_][A-Za-z0-9_]*\.)+(Agent|Tool|Toolset|Context|Corpus|Retriever|Compactor)\b",
|
|
619
|
+
function_call_text,
|
|
620
|
+
)
|
|
621
|
+
if match:
|
|
622
|
+
return match.group(1)
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
def _track_retriever_alias(self, assignment_name: str, expression_list) -> None:
|
|
626
|
+
"""Track retriever constructor aliases from require(...) assignments."""
|
|
627
|
+
try:
|
|
628
|
+
expressions = expression_list.exp()
|
|
629
|
+
except Exception:
|
|
630
|
+
return
|
|
631
|
+
if not expressions:
|
|
632
|
+
return
|
|
633
|
+
expression_node = expressions[0]
|
|
634
|
+
get_text = getattr(expression_node, "getText", None)
|
|
635
|
+
if not callable(get_text):
|
|
636
|
+
return
|
|
637
|
+
expression_text = get_text()
|
|
638
|
+
match = re.match(
|
|
639
|
+
r'require\("tactus\.retrievers\.([A-Za-z0-9_]+)"\)(?:\.Retriever)?$',
|
|
640
|
+
expression_text,
|
|
641
|
+
)
|
|
642
|
+
if not match:
|
|
643
|
+
return
|
|
644
|
+
module_name = match.group(1)
|
|
645
|
+
retriever_id = module_name.replace("_", "-")
|
|
646
|
+
if expression_text.endswith(".Retriever"):
|
|
647
|
+
self._retriever_aliases[assignment_name] = retriever_id
|
|
648
|
+
else:
|
|
649
|
+
self._retriever_modules[assignment_name] = retriever_id
|
|
650
|
+
|
|
651
|
+
def _resolve_retriever_id_from_call(
|
|
652
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
653
|
+
) -> Optional[str]:
|
|
654
|
+
function_name = self._extract_function_name(function_call_context)
|
|
655
|
+
if function_name and function_name in self._retriever_aliases:
|
|
656
|
+
return self._retriever_aliases[function_name]
|
|
657
|
+
call_text = function_call_context.getText()
|
|
658
|
+
dotted_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\.Retriever\b", call_text)
|
|
659
|
+
if dotted_match:
|
|
660
|
+
module_name = dotted_match.group(1)
|
|
661
|
+
return self._retriever_modules.get(module_name)
|
|
662
|
+
return None
|
|
663
|
+
|
|
664
|
+
def _parse_functioncall_expression(
|
|
665
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
666
|
+
) -> Any:
|
|
667
|
+
"""Parse function calls used inside DSL expressions (e.g., context messages)."""
|
|
668
|
+
function_name = self._extract_function_name(function_call_context)
|
|
669
|
+
if not function_name:
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
args = self._extract_arguments(function_call_context)
|
|
673
|
+
|
|
674
|
+
if function_name == "template":
|
|
675
|
+
template_text = args[0] if args else ""
|
|
676
|
+
vars_dict = args[1] if len(args) > 1 else {}
|
|
677
|
+
if not isinstance(template_text, str):
|
|
678
|
+
return None
|
|
679
|
+
if not isinstance(vars_dict, dict):
|
|
680
|
+
vars_dict = {}
|
|
681
|
+
return {"template": template_text, "vars": vars_dict}
|
|
682
|
+
|
|
683
|
+
if function_name in {"system", "user", "assistant"}:
|
|
684
|
+
if not args:
|
|
685
|
+
return None
|
|
686
|
+
arg = args[0]
|
|
687
|
+
if isinstance(arg, dict) and "template" in arg:
|
|
688
|
+
return {
|
|
689
|
+
"type": function_name,
|
|
690
|
+
"template": arg.get("template"),
|
|
691
|
+
"vars": arg.get("vars", {}),
|
|
692
|
+
}
|
|
693
|
+
if isinstance(arg, str):
|
|
694
|
+
return {"type": function_name, "content": arg}
|
|
695
|
+
return {"type": function_name, "content": str(arg)}
|
|
696
|
+
|
|
697
|
+
if function_name == "context":
|
|
698
|
+
if not args:
|
|
699
|
+
return None
|
|
700
|
+
pack_name = args[0]
|
|
701
|
+
budget = args[1] if len(args) > 1 else None
|
|
702
|
+
directive: dict[str, Any] = {
|
|
703
|
+
"type": "context",
|
|
704
|
+
"name": pack_name if isinstance(pack_name, str) else str(pack_name),
|
|
705
|
+
}
|
|
706
|
+
if isinstance(budget, dict):
|
|
707
|
+
directive["budget"] = budget
|
|
708
|
+
return directive
|
|
709
|
+
|
|
710
|
+
if function_name == "history":
|
|
711
|
+
return {"type": "history"}
|
|
712
|
+
|
|
713
|
+
return None
|
|
714
|
+
|
|
376
715
|
def _process_dsl_call(self, function_name: str, ctx: LuaParser.FunctioncallContext):
|
|
377
716
|
"""Extract arguments and register declaration."""
|
|
378
717
|
argument_values = self._extract_arguments(ctx)
|
|
@@ -742,6 +1081,9 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
742
1081
|
return True
|
|
743
1082
|
elif ctx.tableconstructor():
|
|
744
1083
|
return self._parse_table_constructor(ctx.tableconstructor())
|
|
1084
|
+
function_call = getattr(ctx, "functioncall", None)
|
|
1085
|
+
if callable(function_call):
|
|
1086
|
+
return self._parse_functioncall_expression(function_call())
|
|
745
1087
|
|
|
746
1088
|
# For other expressions, return None (can't evaluate without execution)
|
|
747
1089
|
return None
|
|
@@ -771,23 +1113,23 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
771
1113
|
return token_text[2:-2]
|
|
772
1114
|
elif token_text.startswith('"') and token_text.endswith('"'):
|
|
773
1115
|
# Double-quoted string
|
|
774
|
-
|
|
775
|
-
content = content.replace("\\n", "\n")
|
|
776
|
-
content = content.replace("\\t", "\t")
|
|
777
|
-
content = content.replace('\\"', '"')
|
|
778
|
-
content = content.replace("\\\\", "\\")
|
|
779
|
-
return content
|
|
1116
|
+
return self._unescape_basic_string(token_text[1:-1], '"')
|
|
780
1117
|
elif token_text.startswith("'") and token_text.endswith("'"):
|
|
781
1118
|
# Single-quoted string
|
|
782
|
-
|
|
783
|
-
content = content.replace("\\n", "\n")
|
|
784
|
-
content = content.replace("\\t", "\t")
|
|
785
|
-
content = content.replace("\\'", "'")
|
|
786
|
-
content = content.replace("\\\\", "\\")
|
|
787
|
-
return content
|
|
1119
|
+
return self._unescape_basic_string(token_text[1:-1], "'")
|
|
788
1120
|
|
|
789
1121
|
return token_text
|
|
790
1122
|
|
|
1123
|
+
def _unescape_basic_string(self, content: str, quote_char: str) -> str:
|
|
1124
|
+
content = content.replace("\\n", "\n")
|
|
1125
|
+
content = content.replace("\\t", "\t")
|
|
1126
|
+
if quote_char == '"':
|
|
1127
|
+
content = content.replace('\\"', '"')
|
|
1128
|
+
elif quote_char == "'":
|
|
1129
|
+
content = content.replace("\\'", "'")
|
|
1130
|
+
content = content.replace("\\\\", "\\")
|
|
1131
|
+
return content
|
|
1132
|
+
|
|
791
1133
|
def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) -> Any:
|
|
792
1134
|
"""Parse Lua table constructor to Python dict."""
|
|
793
1135
|
parsed_table = {}
|