tactus 0.36.0__py3-none-any.whl → 0.38.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 +22 -2
- tactus/adapters/channels/broker.py +1 -0
- tactus/adapters/channels/host.py +3 -1
- tactus/adapters/channels/ipc.py +18 -3
- tactus/adapters/channels/sse.py +2 -0
- tactus/adapters/mcp_manager.py +24 -7
- tactus/backends/http_backend.py +2 -2
- tactus/backends/pytorch_backend.py +2 -2
- tactus/broker/client.py +3 -3
- tactus/broker/server.py +17 -5
- 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 +560 -20
- tactus/core/exceptions.py +8 -0
- tactus/core/execution_context.py +24 -24
- tactus/core/message_history_manager.py +2 -2
- tactus/core/mocking.py +12 -0
- tactus/core/output_validator.py +6 -6
- tactus/core/registry.py +171 -29
- tactus/core/retrieval.py +317 -0
- tactus/core/retriever_tasks.py +30 -0
- tactus/core/runtime.py +431 -117
- tactus/dspy/agent.py +143 -82
- tactus/dspy/broker_lm.py +13 -7
- tactus/dspy/config.py +23 -4
- tactus/dspy/module.py +12 -1
- tactus/ide/coding_assistant.py +2 -2
- tactus/primitives/handles.py +79 -7
- tactus/primitives/model.py +1 -1
- tactus/primitives/procedure.py +1 -1
- tactus/primitives/state.py +2 -2
- tactus/sandbox/config.py +1 -1
- tactus/sandbox/container_runner.py +13 -6
- 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 +189 -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 +10 -6
- tactus/testing/evaluation_runner.py +5 -5
- tactus/testing/steps/builtin.py +2 -2
- tactus/testing/test_runner.py +6 -4
- tactus/utils/asyncio_helpers.py +2 -1
- tactus/validation/semantic_visitor.py +357 -6
- tactus/validation/validator.py +142 -2
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/METADATA +9 -6
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/RECORD +65 -47
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.36.0.dist-info → tactus-0.38.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,6 +129,7 @@ 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
134
|
# Check if this is a DSL setting assignment
|
|
108
135
|
setting_handlers_by_name = {
|
|
@@ -131,6 +158,16 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
131
158
|
assignment_target_name, first_expression
|
|
132
159
|
)
|
|
133
160
|
|
|
161
|
+
attname_list = getattr(context, "attnamelist", None)
|
|
162
|
+
exp_list = getattr(context, "explist", None)
|
|
163
|
+
if callable(attname_list) and callable(exp_list):
|
|
164
|
+
attname_nodes = attname_list()
|
|
165
|
+
exp_nodes = exp_list()
|
|
166
|
+
if attname_nodes and exp_nodes:
|
|
167
|
+
name_nodes = attname_nodes.NAME()
|
|
168
|
+
if name_nodes:
|
|
169
|
+
self._track_retriever_alias(name_nodes[0].getText(), exp_nodes)
|
|
170
|
+
|
|
134
171
|
# Continue visiting children
|
|
135
172
|
return self.visitChildren(context)
|
|
136
173
|
|
|
@@ -144,12 +181,43 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
144
181
|
if prefix_expression.functioncall():
|
|
145
182
|
function_call = prefix_expression.functioncall()
|
|
146
183
|
function_name = self._extract_function_name(function_call)
|
|
184
|
+
dotted_name = None
|
|
185
|
+
if function_name not in {
|
|
186
|
+
"Agent",
|
|
187
|
+
"Tool",
|
|
188
|
+
"Toolset",
|
|
189
|
+
"Context",
|
|
190
|
+
"Corpus",
|
|
191
|
+
"Retriever",
|
|
192
|
+
"Compactor",
|
|
193
|
+
}:
|
|
194
|
+
dotted_name = self._extract_dotted_dsl_name(function_call)
|
|
195
|
+
if dotted_name:
|
|
196
|
+
function_name = dotted_name
|
|
197
|
+
else:
|
|
198
|
+
dotted_name = self._extract_dotted_dsl_name(function_call)
|
|
199
|
+
|
|
200
|
+
# Check if this is a chained method call (e.g., Agent('name').turn()).
|
|
201
|
+
# Dotted module calls like vector.Corpus { ... } are not chained.
|
|
202
|
+
get_text = getattr(function_call, "getText", None)
|
|
203
|
+
function_call_text = get_text() if callable(get_text) else ""
|
|
204
|
+
is_chained_method_call = (
|
|
205
|
+
re.search(r"[)}\\]]\\s*\\.", function_call_text) is not None
|
|
206
|
+
)
|
|
147
207
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
if function_name in {"Corpus", "Retriever"}:
|
|
209
|
+
has_namespace = dotted_name is not None or re.match(
|
|
210
|
+
r"[A-Za-z_][A-Za-z0-9_]*\\.", function_call_text
|
|
211
|
+
)
|
|
212
|
+
if not has_namespace:
|
|
213
|
+
self._record_error(
|
|
214
|
+
message=(
|
|
215
|
+
f"Direct {function_name} declarations are not supported. "
|
|
216
|
+
"Use module.Corpus { ... } instead."
|
|
217
|
+
),
|
|
218
|
+
declaration=function_name,
|
|
219
|
+
)
|
|
220
|
+
return
|
|
153
221
|
|
|
154
222
|
if function_name == "Agent" and not is_chained_method_call:
|
|
155
223
|
# Extract config from Agent {...}
|
|
@@ -194,6 +262,37 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
194
262
|
assignment_target_name,
|
|
195
263
|
declaration_config if declaration_config else {},
|
|
196
264
|
)
|
|
265
|
+
elif function_name == "Context":
|
|
266
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
267
|
+
self.builder.register_context(
|
|
268
|
+
assignment_target_name,
|
|
269
|
+
declaration_config if declaration_config else {},
|
|
270
|
+
)
|
|
271
|
+
elif function_name == "Corpus":
|
|
272
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
273
|
+
self.builder.register_corpus(
|
|
274
|
+
assignment_target_name,
|
|
275
|
+
declaration_config if declaration_config else {},
|
|
276
|
+
)
|
|
277
|
+
elif function_name == "Retriever":
|
|
278
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
279
|
+
retriever_id = self._resolve_retriever_id_from_call(function_call)
|
|
280
|
+
if (
|
|
281
|
+
isinstance(declaration_config, dict)
|
|
282
|
+
and retriever_id
|
|
283
|
+
and "retriever_id" not in declaration_config
|
|
284
|
+
):
|
|
285
|
+
declaration_config["retriever_id"] = retriever_id
|
|
286
|
+
self.builder.register_retriever(
|
|
287
|
+
assignment_target_name,
|
|
288
|
+
declaration_config if declaration_config else {},
|
|
289
|
+
)
|
|
290
|
+
elif function_name == "Compactor":
|
|
291
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
292
|
+
self.builder.register_compactor(
|
|
293
|
+
assignment_target_name,
|
|
294
|
+
declaration_config if declaration_config else {},
|
|
295
|
+
)
|
|
197
296
|
elif function_name == "Procedure":
|
|
198
297
|
# New assignment syntax: main = Procedure { function(input) ... }
|
|
199
298
|
# Register as a named procedure
|
|
@@ -204,10 +303,29 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
204
303
|
{}, # Output schema will be extracted from top-level output {}
|
|
205
304
|
{}, # State schema
|
|
206
305
|
)
|
|
306
|
+
elif function_name == "Task":
|
|
307
|
+
# Assignment-based task declaration: name = Task { ... }
|
|
308
|
+
self._processed_task_calls.add(id(function_call))
|
|
309
|
+
self._register_task_declaration(assignment_target_name, function_call)
|
|
310
|
+
else:
|
|
311
|
+
# Heuristic: treat any assignment-based call with a 'corpus' field as a retriever.
|
|
312
|
+
declaration_config = self._extract_single_table_arg(function_call)
|
|
313
|
+
if isinstance(declaration_config, dict) and "corpus" in declaration_config:
|
|
314
|
+
retriever_id = self._resolve_retriever_id_from_call(function_call)
|
|
315
|
+
if retriever_id and "retriever_id" not in declaration_config:
|
|
316
|
+
declaration_config["retriever_id"] = retriever_id
|
|
317
|
+
self.builder.register_retriever(
|
|
318
|
+
assignment_target_name,
|
|
319
|
+
declaration_config,
|
|
320
|
+
)
|
|
207
321
|
|
|
208
322
|
def _extract_single_table_arg(self, function_call) -> dict:
|
|
209
323
|
"""Extract a single table argument from a function call like Agent {...}."""
|
|
210
|
-
|
|
324
|
+
args_method = getattr(function_call, "args", None)
|
|
325
|
+
if args_method is None:
|
|
326
|
+
return {}
|
|
327
|
+
|
|
328
|
+
argument_list_nodes = args_method()
|
|
211
329
|
if not argument_list_nodes:
|
|
212
330
|
return {}
|
|
213
331
|
|
|
@@ -239,6 +357,14 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
239
357
|
if function_name in self.DSL_FUNCTIONS and not is_method_access_call:
|
|
240
358
|
# Process the DSL call (but skip method calls like Tool.called())
|
|
241
359
|
try:
|
|
360
|
+
if function_name == "Task":
|
|
361
|
+
if id(context) in self._processed_task_calls:
|
|
362
|
+
return None
|
|
363
|
+
self._register_task_declaration(None, context)
|
|
364
|
+
return None
|
|
365
|
+
if function_name == "IncludeTasks":
|
|
366
|
+
self._register_include_tasks(context)
|
|
367
|
+
return None
|
|
242
368
|
self._process_dsl_call(function_name, context)
|
|
243
369
|
except Exception as processing_exception:
|
|
244
370
|
self.errors.append(
|
|
@@ -254,6 +380,122 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
254
380
|
|
|
255
381
|
return self.visitChildren(context)
|
|
256
382
|
|
|
383
|
+
def _register_task_declaration(
|
|
384
|
+
self,
|
|
385
|
+
assignment_name: Optional[str],
|
|
386
|
+
function_call_context: LuaParser.FunctioncallContext,
|
|
387
|
+
parent_task: Optional[str] = None,
|
|
388
|
+
) -> None:
|
|
389
|
+
args = self._extract_arguments(function_call_context)
|
|
390
|
+
task_name = None
|
|
391
|
+
task_config: dict[str, Any] = {}
|
|
392
|
+
|
|
393
|
+
if args:
|
|
394
|
+
if isinstance(args[0], str):
|
|
395
|
+
task_name = args[0]
|
|
396
|
+
elif isinstance(args[0], dict):
|
|
397
|
+
task_config = args[0]
|
|
398
|
+
|
|
399
|
+
if len(args) >= 2 and isinstance(args[1], dict):
|
|
400
|
+
task_config = args[1]
|
|
401
|
+
|
|
402
|
+
if assignment_name and task_name and assignment_name != task_name:
|
|
403
|
+
self._record_error(
|
|
404
|
+
message=(
|
|
405
|
+
f"Task name mismatch: '{assignment_name} = Task \"{task_name}\" {{ ... }}'. "
|
|
406
|
+
f"Remove the string name or set it to '{assignment_name}'."
|
|
407
|
+
),
|
|
408
|
+
declaration="Task",
|
|
409
|
+
)
|
|
410
|
+
task_name = assignment_name
|
|
411
|
+
|
|
412
|
+
if assignment_name and not task_name:
|
|
413
|
+
task_name = assignment_name
|
|
414
|
+
|
|
415
|
+
if not task_name:
|
|
416
|
+
self._record_error("Task name is required.", declaration="Task")
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
children = self._extract_nested_tasks(function_call_context)
|
|
420
|
+
for child in children:
|
|
421
|
+
child_name = child["name"]
|
|
422
|
+
self.builder.register_task(child_name, child.get("config", {}), parent=task_name)
|
|
423
|
+
|
|
424
|
+
self.builder.register_task(task_name, task_config, parent=parent_task)
|
|
425
|
+
|
|
426
|
+
def _extract_nested_tasks(
|
|
427
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
428
|
+
) -> list[dict[str, Any]]:
|
|
429
|
+
"""Extract nested Task calls from a Task { ... } table constructor."""
|
|
430
|
+
nested_tasks: list[dict[str, Any]] = []
|
|
431
|
+
argument_list_nodes = function_call_context.args()
|
|
432
|
+
if not argument_list_nodes:
|
|
433
|
+
return nested_tasks
|
|
434
|
+
|
|
435
|
+
for args_ctx in argument_list_nodes:
|
|
436
|
+
table_ctx = args_ctx.tableconstructor()
|
|
437
|
+
if not table_ctx or not table_ctx.fieldlist():
|
|
438
|
+
continue
|
|
439
|
+
for field in table_ctx.fieldlist().field():
|
|
440
|
+
if len(field.exp()) != 1:
|
|
441
|
+
continue
|
|
442
|
+
child_exp = field.exp(0)
|
|
443
|
+
child_call = None
|
|
444
|
+
if hasattr(child_exp, "functioncall") and child_exp.functioncall():
|
|
445
|
+
child_call = child_exp.functioncall()
|
|
446
|
+
elif child_exp.prefixexp() and child_exp.prefixexp().functioncall():
|
|
447
|
+
child_call = child_exp.prefixexp().functioncall()
|
|
448
|
+
if not child_call:
|
|
449
|
+
continue
|
|
450
|
+
child_name = self._extract_function_name(child_call)
|
|
451
|
+
if child_name != "Task":
|
|
452
|
+
continue
|
|
453
|
+
child_args = self._extract_arguments(child_call)
|
|
454
|
+
child_task_name = None
|
|
455
|
+
if field.NAME():
|
|
456
|
+
child_task_name = field.NAME().getText()
|
|
457
|
+
if not child_task_name and child_args:
|
|
458
|
+
child_task_name = child_args[0] if isinstance(child_args[0], str) else None
|
|
459
|
+
child_task_config = {}
|
|
460
|
+
if len(child_args) >= 2 and isinstance(child_args[1], dict):
|
|
461
|
+
child_task_config = child_args[1]
|
|
462
|
+
if field.NAME() and child_args and isinstance(child_args[0], str):
|
|
463
|
+
if field.NAME().getText() != child_args[0]:
|
|
464
|
+
self._record_error(
|
|
465
|
+
message=(
|
|
466
|
+
f"Task name mismatch: '{field.NAME().getText()} = Task \"{child_args[0]}\" {{ ... }}'. "
|
|
467
|
+
f"Remove the string name or set it to '{field.NAME().getText()}'."
|
|
468
|
+
),
|
|
469
|
+
declaration="Task",
|
|
470
|
+
)
|
|
471
|
+
if child_task_name:
|
|
472
|
+
nested_tasks.append(
|
|
473
|
+
{
|
|
474
|
+
"name": child_task_name,
|
|
475
|
+
"config": child_task_config,
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
return nested_tasks
|
|
479
|
+
|
|
480
|
+
def _register_include_tasks(self, context: LuaParser.FunctioncallContext) -> None:
|
|
481
|
+
args = self._extract_arguments(context)
|
|
482
|
+
if not args:
|
|
483
|
+
self._record_error("IncludeTasks requires a path string.", declaration="IncludeTasks")
|
|
484
|
+
return
|
|
485
|
+
if not isinstance(args[0], str):
|
|
486
|
+
self._record_error(
|
|
487
|
+
"IncludeTasks path must be a string literal.",
|
|
488
|
+
declaration="IncludeTasks",
|
|
489
|
+
)
|
|
490
|
+
return
|
|
491
|
+
namespace = None
|
|
492
|
+
if len(args) >= 2:
|
|
493
|
+
if isinstance(args[1], str):
|
|
494
|
+
namespace = args[1]
|
|
495
|
+
elif isinstance(args[1], dict):
|
|
496
|
+
namespace = args[1].get("namespace")
|
|
497
|
+
self.builder.register_include_tasks(args[0], namespace)
|
|
498
|
+
|
|
257
499
|
def _check_deprecated_method_calls(self, ctx: LuaParser.FunctioncallContext):
|
|
258
500
|
"""Check for deprecated method calls like .turn() or .run()."""
|
|
259
501
|
# Method calls have the form: varOrExp nameAndArgs+
|
|
@@ -373,6 +615,112 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
373
615
|
|
|
374
616
|
return None
|
|
375
617
|
|
|
618
|
+
def _extract_dotted_dsl_name(
|
|
619
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
620
|
+
) -> Optional[str]:
|
|
621
|
+
"""Extract DSL names from dotted calls like module.Corpus {...}."""
|
|
622
|
+
get_text = getattr(function_call_context, "getText", None)
|
|
623
|
+
if not callable(get_text):
|
|
624
|
+
return None
|
|
625
|
+
function_call_text = get_text()
|
|
626
|
+
match = re.match(
|
|
627
|
+
r"(?:[A-Za-z_][A-Za-z0-9_]*\.)+(Agent|Tool|Toolset|Context|Corpus|Retriever|Compactor)\b",
|
|
628
|
+
function_call_text,
|
|
629
|
+
)
|
|
630
|
+
if match:
|
|
631
|
+
return match.group(1)
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
def _track_retriever_alias(self, assignment_name: str, expression_list) -> None:
|
|
635
|
+
"""Track retriever constructor aliases from require(...) assignments."""
|
|
636
|
+
try:
|
|
637
|
+
expressions = expression_list.exp()
|
|
638
|
+
except Exception:
|
|
639
|
+
return
|
|
640
|
+
if not expressions:
|
|
641
|
+
return
|
|
642
|
+
expression_node = expressions[0]
|
|
643
|
+
get_text = getattr(expression_node, "getText", None)
|
|
644
|
+
if not callable(get_text):
|
|
645
|
+
return
|
|
646
|
+
expression_text = get_text()
|
|
647
|
+
match = re.match(
|
|
648
|
+
r'require\("tactus\.retrievers\.([A-Za-z0-9_]+)"\)(?:\.Retriever)?$',
|
|
649
|
+
expression_text,
|
|
650
|
+
)
|
|
651
|
+
if not match:
|
|
652
|
+
return
|
|
653
|
+
module_name = match.group(1)
|
|
654
|
+
retriever_id = module_name.replace("_", "-")
|
|
655
|
+
if expression_text.endswith(".Retriever"):
|
|
656
|
+
self._retriever_aliases[assignment_name] = retriever_id
|
|
657
|
+
else:
|
|
658
|
+
self._retriever_modules[assignment_name] = retriever_id
|
|
659
|
+
|
|
660
|
+
def _resolve_retriever_id_from_call(
|
|
661
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
662
|
+
) -> Optional[str]:
|
|
663
|
+
function_name = self._extract_function_name(function_call_context)
|
|
664
|
+
if function_name and function_name in self._retriever_aliases:
|
|
665
|
+
return self._retriever_aliases[function_name]
|
|
666
|
+
call_text = function_call_context.getText()
|
|
667
|
+
dotted_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\.Retriever\b", call_text)
|
|
668
|
+
if dotted_match:
|
|
669
|
+
module_name = dotted_match.group(1)
|
|
670
|
+
return self._retriever_modules.get(module_name)
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
def _parse_functioncall_expression(
|
|
674
|
+
self, function_call_context: LuaParser.FunctioncallContext
|
|
675
|
+
) -> Any:
|
|
676
|
+
"""Parse function calls used inside DSL expressions (e.g., context messages)."""
|
|
677
|
+
function_name = self._extract_function_name(function_call_context)
|
|
678
|
+
if not function_name:
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
args = self._extract_arguments(function_call_context)
|
|
682
|
+
|
|
683
|
+
if function_name == "template":
|
|
684
|
+
template_text = args[0] if args else ""
|
|
685
|
+
vars_dict = args[1] if len(args) > 1 else {}
|
|
686
|
+
if not isinstance(template_text, str):
|
|
687
|
+
return None
|
|
688
|
+
if not isinstance(vars_dict, dict):
|
|
689
|
+
vars_dict = {}
|
|
690
|
+
return {"template": template_text, "vars": vars_dict}
|
|
691
|
+
|
|
692
|
+
if function_name in {"system", "user", "assistant"}:
|
|
693
|
+
if not args:
|
|
694
|
+
return None
|
|
695
|
+
arg = args[0]
|
|
696
|
+
if isinstance(arg, dict) and "template" in arg:
|
|
697
|
+
return {
|
|
698
|
+
"type": function_name,
|
|
699
|
+
"template": arg.get("template"),
|
|
700
|
+
"vars": arg.get("vars", {}),
|
|
701
|
+
}
|
|
702
|
+
if isinstance(arg, str):
|
|
703
|
+
return {"type": function_name, "content": arg}
|
|
704
|
+
return {"type": function_name, "content": str(arg)}
|
|
705
|
+
|
|
706
|
+
if function_name == "context":
|
|
707
|
+
if not args:
|
|
708
|
+
return None
|
|
709
|
+
pack_name = args[0]
|
|
710
|
+
budget = args[1] if len(args) > 1 else None
|
|
711
|
+
directive: dict[str, Any] = {
|
|
712
|
+
"type": "context",
|
|
713
|
+
"name": pack_name if isinstance(pack_name, str) else str(pack_name),
|
|
714
|
+
}
|
|
715
|
+
if isinstance(budget, dict):
|
|
716
|
+
directive["budget"] = budget
|
|
717
|
+
return directive
|
|
718
|
+
|
|
719
|
+
if function_name == "history":
|
|
720
|
+
return {"type": "history"}
|
|
721
|
+
|
|
722
|
+
return None
|
|
723
|
+
|
|
376
724
|
def _process_dsl_call(self, function_name: str, ctx: LuaParser.FunctioncallContext):
|
|
377
725
|
"""Extract arguments and register declaration."""
|
|
378
726
|
argument_values = self._extract_arguments(ctx)
|
|
@@ -742,6 +1090,9 @@ class TactusDSLVisitor(LuaParserVisitor):
|
|
|
742
1090
|
return True
|
|
743
1091
|
elif ctx.tableconstructor():
|
|
744
1092
|
return self._parse_table_constructor(ctx.tableconstructor())
|
|
1093
|
+
function_call = getattr(ctx, "functioncall", None)
|
|
1094
|
+
if callable(function_call):
|
|
1095
|
+
return self._parse_functioncall_expression(function_call())
|
|
745
1096
|
|
|
746
1097
|
# For other expressions, return None (can't evaluate without execution)
|
|
747
1098
|
return None
|
tactus/validation/validator.py
CHANGED
|
@@ -9,6 +9,7 @@ Validates .tac files using ANTLR parser:
|
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
11
|
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
from typing import Optional
|
|
13
14
|
|
|
14
15
|
from antlr4 import InputStream, CommonTokenStream
|
|
@@ -16,7 +17,7 @@ from .generated.LuaLexer import LuaLexer
|
|
|
16
17
|
from .generated.LuaParser import LuaParser
|
|
17
18
|
from .semantic_visitor import TactusDSLVisitor
|
|
18
19
|
from .error_listener import TactusErrorListener
|
|
19
|
-
from tactus.core.registry import ValidationResult, ValidationMessage
|
|
20
|
+
from tactus.core.registry import ValidationResult, ValidationMessage, TaskDeclaration
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
@@ -175,7 +176,101 @@ class TactusValidator:
|
|
|
175
176
|
try:
|
|
176
177
|
with open(file_path, "r") as source_file_handle:
|
|
177
178
|
source_text = source_file_handle.read()
|
|
178
|
-
|
|
179
|
+
primary_result = self.validate(source_text, mode)
|
|
180
|
+
if not primary_result.valid or mode == ValidationMode.QUICK:
|
|
181
|
+
return primary_result
|
|
182
|
+
registry = primary_result.registry
|
|
183
|
+
if not registry or not registry.include_tasks:
|
|
184
|
+
return primary_result
|
|
185
|
+
|
|
186
|
+
base_path = Path(file_path).parent
|
|
187
|
+
merged_registry = registry
|
|
188
|
+
include_queue = [
|
|
189
|
+
{
|
|
190
|
+
"path": include.get("path"),
|
|
191
|
+
"namespace": include.get("namespace"),
|
|
192
|
+
"base": base_path,
|
|
193
|
+
}
|
|
194
|
+
for include in registry.include_tasks
|
|
195
|
+
]
|
|
196
|
+
seen_includes: set[Path] = set()
|
|
197
|
+
|
|
198
|
+
while include_queue:
|
|
199
|
+
include = include_queue.pop(0)
|
|
200
|
+
include_path = include.get("path")
|
|
201
|
+
if not include_path:
|
|
202
|
+
continue
|
|
203
|
+
include_base = include.get("base") or base_path
|
|
204
|
+
include_file = (include_base / include_path).resolve()
|
|
205
|
+
if include_file in seen_includes:
|
|
206
|
+
return self._result_with_errors(
|
|
207
|
+
errors=[
|
|
208
|
+
ValidationMessage(
|
|
209
|
+
level="error",
|
|
210
|
+
message=f"IncludeTasks cycle detected: {include_file}",
|
|
211
|
+
)
|
|
212
|
+
]
|
|
213
|
+
)
|
|
214
|
+
seen_includes.add(include_file)
|
|
215
|
+
if not include_file.exists():
|
|
216
|
+
return self._result_with_errors(
|
|
217
|
+
errors=[
|
|
218
|
+
ValidationMessage(
|
|
219
|
+
level="error",
|
|
220
|
+
message=f"Included tasks file not found: {include_file}",
|
|
221
|
+
)
|
|
222
|
+
]
|
|
223
|
+
)
|
|
224
|
+
with open(include_file, "r") as include_handle:
|
|
225
|
+
include_source = include_handle.read()
|
|
226
|
+
include_result = self.validate(include_source, mode)
|
|
227
|
+
if not include_result.valid or not include_result.registry:
|
|
228
|
+
return include_result
|
|
229
|
+
if self._include_has_non_task_declarations(include_result.registry):
|
|
230
|
+
return self._result_with_errors(
|
|
231
|
+
errors=[
|
|
232
|
+
ValidationMessage(
|
|
233
|
+
level="error",
|
|
234
|
+
message=(
|
|
235
|
+
"IncludeTasks files must only contain Task declarations: "
|
|
236
|
+
f"{include_file}"
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
]
|
|
240
|
+
)
|
|
241
|
+
try:
|
|
242
|
+
self._merge_tasks(
|
|
243
|
+
merged_registry,
|
|
244
|
+
include_result.registry,
|
|
245
|
+
namespace=include.get("namespace"),
|
|
246
|
+
)
|
|
247
|
+
except ValueError as merge_error:
|
|
248
|
+
return self._result_with_errors(
|
|
249
|
+
errors=[
|
|
250
|
+
ValidationMessage(
|
|
251
|
+
level="error",
|
|
252
|
+
message=str(merge_error),
|
|
253
|
+
)
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if include_result.registry.include_tasks:
|
|
258
|
+
include_queue.extend(
|
|
259
|
+
[
|
|
260
|
+
{
|
|
261
|
+
"path": nested.get("path"),
|
|
262
|
+
"namespace": nested.get("namespace"),
|
|
263
|
+
"base": include_file.parent,
|
|
264
|
+
}
|
|
265
|
+
for nested in include_result.registry.include_tasks
|
|
266
|
+
]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return self._result_success(
|
|
270
|
+
errors=primary_result.errors,
|
|
271
|
+
warnings=primary_result.warnings,
|
|
272
|
+
registry=merged_registry,
|
|
273
|
+
)
|
|
179
274
|
except FileNotFoundError:
|
|
180
275
|
return self._result_with_errors(
|
|
181
276
|
errors=[
|
|
@@ -194,3 +289,48 @@ class TactusValidator:
|
|
|
194
289
|
)
|
|
195
290
|
]
|
|
196
291
|
)
|
|
292
|
+
|
|
293
|
+
def _include_has_non_task_declarations(self, registry) -> bool:
|
|
294
|
+
if registry is None:
|
|
295
|
+
return False
|
|
296
|
+
return any(
|
|
297
|
+
[
|
|
298
|
+
registry.agents,
|
|
299
|
+
registry.toolsets,
|
|
300
|
+
registry.lua_tools,
|
|
301
|
+
registry.contexts,
|
|
302
|
+
registry.corpora,
|
|
303
|
+
registry.retrievers,
|
|
304
|
+
registry.compactors,
|
|
305
|
+
registry.named_procedures,
|
|
306
|
+
registry.specifications,
|
|
307
|
+
registry.dependencies,
|
|
308
|
+
registry.models,
|
|
309
|
+
registry.hitl_points,
|
|
310
|
+
registry.mocks,
|
|
311
|
+
registry.agent_mocks,
|
|
312
|
+
registry.prompts,
|
|
313
|
+
]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _merge_tasks(
|
|
317
|
+
self,
|
|
318
|
+
target_registry,
|
|
319
|
+
include_registry,
|
|
320
|
+
namespace: Optional[str] = None,
|
|
321
|
+
) -> None:
|
|
322
|
+
if not include_registry.tasks:
|
|
323
|
+
return
|
|
324
|
+
if namespace:
|
|
325
|
+
if namespace in target_registry.tasks:
|
|
326
|
+
raise ValueError(f"Duplicate task namespace '{namespace}'")
|
|
327
|
+
target_registry.tasks[namespace] = TaskDeclaration(
|
|
328
|
+
name=namespace,
|
|
329
|
+
children=include_registry.tasks,
|
|
330
|
+
)
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
for task_name, task in include_registry.tasks.items():
|
|
334
|
+
if task_name in target_registry.tasks:
|
|
335
|
+
raise ValueError(f"Duplicate task '{task_name}'")
|
|
336
|
+
target_registry.tasks[task_name] = task
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tactus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.0
|
|
4
4
|
Summary: Tactus: Lua-based DSL for agentic workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/AnthusAI/Tactus
|
|
6
6
|
Project-URL: Documentation, https://github.com/AnthusAI/Tactus/tree/main/docs
|
|
@@ -14,15 +14,18 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
21
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.9
|
|
22
24
|
Requires-Dist: antlr4-python3-runtime==4.13.1
|
|
23
25
|
Requires-Dist: behave>=1.2.6
|
|
26
|
+
Requires-Dist: biblicus>=1.0.0
|
|
24
27
|
Requires-Dist: boto3>=1.28.0
|
|
25
|
-
Requires-Dist: dotyaml>=0.1.
|
|
28
|
+
Requires-Dist: dotyaml>=0.1.4
|
|
26
29
|
Requires-Dist: dspy>=2.5
|
|
27
30
|
Requires-Dist: flask-cors>=4.0.0
|
|
28
31
|
Requires-Dist: flask>=3.0.0
|
|
@@ -39,7 +42,7 @@ Requires-Dist: openpyxl>=3.1
|
|
|
39
42
|
Requires-Dist: opentelemetry-api>=1.39.1
|
|
40
43
|
Requires-Dist: opentelemetry-sdk>=1.39.1
|
|
41
44
|
Requires-Dist: pyarrow>=14.0
|
|
42
|
-
Requires-Dist: pydantic-ai
|
|
45
|
+
Requires-Dist: pydantic-ai
|
|
43
46
|
Requires-Dist: pydantic>=2.0
|
|
44
47
|
Requires-Dist: pyyaml
|
|
45
48
|
Requires-Dist: rapidfuzz>=3.0.0
|
|
@@ -48,9 +51,9 @@ Requires-Dist: typer
|
|
|
48
51
|
Provides-Extra: dev
|
|
49
52
|
Requires-Dist: antlr4-tools>=0.2.1; extra == 'dev'
|
|
50
53
|
Requires-Dist: behave>=1.2.6; extra == 'dev'
|
|
51
|
-
Requires-Dist: black==
|
|
54
|
+
Requires-Dist: black==24.10.0; extra == 'dev'
|
|
52
55
|
Requires-Dist: coverage>=7.4; extra == 'dev'
|
|
53
|
-
Requires-Dist: fastmcp>=2.3.5; extra == 'dev'
|
|
56
|
+
Requires-Dist: fastmcp>=2.3.5; (python_version >= '3.10') and extra == 'dev'
|
|
54
57
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
55
58
|
Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
|
|
56
59
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|