tactus 0.37.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.
Files changed (46) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/channels/base.py +2 -0
  3. tactus/cli/app.py +212 -57
  4. tactus/core/compaction.py +17 -0
  5. tactus/core/context_assembler.py +73 -0
  6. tactus/core/context_models.py +41 -0
  7. tactus/core/dsl_stubs.py +557 -17
  8. tactus/core/exceptions.py +8 -0
  9. tactus/core/execution_context.py +1 -1
  10. tactus/core/mocking.py +12 -0
  11. tactus/core/registry.py +142 -0
  12. tactus/core/retrieval.py +317 -0
  13. tactus/core/retriever_tasks.py +30 -0
  14. tactus/core/runtime.py +388 -74
  15. tactus/dspy/agent.py +143 -82
  16. tactus/dspy/config.py +16 -0
  17. tactus/dspy/module.py +12 -1
  18. tactus/ide/coding_assistant.py +2 -2
  19. tactus/primitives/handles.py +79 -7
  20. tactus/sandbox/config.py +1 -1
  21. tactus/sandbox/container_runner.py +2 -0
  22. tactus/sandbox/entrypoint.py +51 -8
  23. tactus/sandbox/protocol.py +5 -0
  24. tactus/stdlib/README.md +10 -1
  25. tactus/stdlib/biblicus/__init__.py +3 -0
  26. tactus/stdlib/biblicus/text.py +189 -0
  27. tactus/stdlib/tac/biblicus/text.tac +32 -0
  28. tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
  29. tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
  30. tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
  31. tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
  32. tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
  33. tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
  34. tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
  35. tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
  36. tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
  37. tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
  38. tactus/testing/behave_integration.py +2 -0
  39. tactus/testing/context.py +4 -0
  40. tactus/validation/semantic_visitor.py +357 -6
  41. tactus/validation/validator.py +142 -2
  42. {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/METADATA +3 -2
  43. {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/RECORD +46 -28
  44. {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
  45. {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
  46. {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,137 @@
1
+ # Retriever Modules
2
+
3
+ Tactus exposes Biblicus-backed retrievers as Lua modules so you can select a retriever explicitly at the top of your `.tac` file.
4
+
5
+ ## Embedding index (file-backed)
6
+
7
+ ```lua
8
+ local FilesystemCorpus = require("tactus.corpora.filesystem")
9
+ local vector = require("tactus.retrievers.embedding_index_file")
10
+
11
+ support_notes = FilesystemCorpus.Corpus {
12
+ root = "corpora/support-notes",
13
+ configuration = {
14
+ pipeline = {
15
+ extract = {
16
+ -- extraction steps (optional)
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ support_search = vector.Retriever {
23
+ corpus = support_notes,
24
+ configuration = {
25
+ pipeline = {
26
+ index = {
27
+ embedding_provider = { provider_id = "hash-embedding", dimensions = 64 }
28
+ },
29
+ query = {
30
+ limit = 3,
31
+ maximum_total_characters = 1200
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Embedding index (in-memory)
39
+
40
+ ```lua
41
+ local FilesystemCorpus = require("tactus.corpora.filesystem")
42
+ local vector = require("tactus.retrievers.embedding_index_inmemory")
43
+
44
+ notes = FilesystemCorpus.Corpus {
45
+ root = "corpora/notes",
46
+ configuration = {
47
+ pipeline = {
48
+ extract = {
49
+ -- extraction steps (optional)
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ search = vector.Retriever {
56
+ corpus = notes,
57
+ configuration = {
58
+ pipeline = {
59
+ index = {
60
+ embedding_provider = { provider_id = "hash-embedding", dimensions = 64 },
61
+ maximum_cache_total_items = 5000
62
+ },
63
+ query = {
64
+ limit = 2
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## SQLite full-text search
72
+
73
+ ```lua
74
+ local FilesystemCorpus = require("tactus.corpora.filesystem")
75
+ local vector = require("tactus.retrievers.sqlite_full_text_search")
76
+
77
+ notes = FilesystemCorpus.Corpus {
78
+ root = "corpora/notes",
79
+ configuration = {
80
+ pipeline = {
81
+ extract = {
82
+ -- extraction steps (optional)
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ search = vector.Retriever {
89
+ corpus = notes,
90
+ configuration = {
91
+ pipeline = {
92
+ index = {
93
+ snippet_characters = 400,
94
+ chunk_size = 800,
95
+ chunk_overlap = 200
96
+ },
97
+ query = {
98
+ limit = 2,
99
+ maximum_total_characters = 1200
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## TF vector (term-frequency)
107
+
108
+ ```lua
109
+ local FilesystemCorpus = require("tactus.corpora.filesystem")
110
+ local vector = require("tactus.retrievers.tf_vector")
111
+
112
+ notes = FilesystemCorpus.Corpus {
113
+ root = "corpora/notes",
114
+ configuration = {
115
+ pipeline = {
116
+ extract = {
117
+ -- extraction steps (optional)
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ search = vector.Retriever {
124
+ corpus = notes,
125
+ configuration = {
126
+ pipeline = {
127
+ index = {
128
+ -- optional index settings
129
+ },
130
+ query = {
131
+ limit = 2,
132
+ maximum_total_characters = 1200
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
@@ -0,0 +1,11 @@
1
+ local embedding_index_file = require("tactus.retrievers.embedding_index_file")
2
+ local embedding_index_inmemory = require("tactus.retrievers.embedding_index_inmemory")
3
+ local sqlite_full_text_search = require("tactus.retrievers.sqlite_full_text_search")
4
+ local tf_vector = require("tactus.retrievers.tf_vector")
5
+
6
+ return {
7
+ EmbeddingIndexFile = embedding_index_file,
8
+ EmbeddingIndexInMemory = embedding_index_inmemory,
9
+ SqliteFullTextSearch = sqlite_full_text_search,
10
+ TfVector = tf_vector,
11
+ }
@@ -0,0 +1,6 @@
1
+ local base = require("tactus.retrievers.base")
2
+
3
+ return {
4
+ Corpus = base.wrap_corpus({}),
5
+ Retriever = base.wrap_retriever({ retriever_id = "sqlite-full-text-search" }),
6
+ }
@@ -0,0 +1,6 @@
1
+ local base = require("tactus.retrievers.base")
2
+
3
+ return {
4
+ Corpus = base.wrap_corpus({}),
5
+ Retriever = base.wrap_retriever({ retriever_id = "tf-vector" }),
6
+ }
@@ -413,6 +413,7 @@ class BehaveEnvironmentGenerator:
413
413
  f.write('"""\n\n')
414
414
 
415
415
  f.write("import sys\n")
416
+ f.write("import os\n")
416
417
  f.write("import json\n")
417
418
  f.write("from pathlib import Path\n\n")
418
419
 
@@ -440,6 +441,7 @@ class BehaveEnvironmentGenerator:
440
441
  f.write(f" context.mcp_servers = json.loads('{mcp_servers_json}')\n")
441
442
  f.write(f" context.tool_paths = json.loads('{tool_paths_json}')\n")
442
443
  f.write(f" context.mocked = {mocked}\n\n")
444
+ f.write(" os.environ['TACTUS_MOCK_MODE'] = '1' if context.mocked else '0'\n\n")
443
445
 
444
446
  f.write("def before_scenario(context, scenario):\n")
445
447
  f.write(' """Setup before each scenario."""\n')
tactus/testing/context.py CHANGED
@@ -171,6 +171,7 @@ class TactusTestContext:
171
171
  from tactus.testing.mock_registry import UnifiedMockRegistry
172
172
  from tactus.adapters.cli_log import CLILogHandler
173
173
 
174
+ os.environ["TACTUS_MOCK_MODE"] = "1" if self.mocked else "0"
174
175
  storage = MemoryStorage()
175
176
 
176
177
  # Setup mock registry if in mocked mode
@@ -209,6 +210,9 @@ class TactusTestContext:
209
210
  from tactus.core.mocking import MockManager
210
211
 
211
212
  self.runtime.mock_manager = MockManager()
213
+ from tactus.core.mocking import set_current_mock_manager
214
+
215
+ set_current_mock_manager(self.runtime.mock_manager)
212
216
  logger.info("Created MockManager for Mocks {} block support")
213
217
  # Mocked-mode tests should never call real LLMs by default.
214
218
  self.runtime.mock_all_agents = True
@@ -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
- # Check if this is a chained method call (e.g., Agent('name').turn())
149
- # Chained calls have structure: func_name args . method_name args
150
- # Simple declarations have: func_name args or func_name table
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
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
- argument_list_nodes = function_call.args()
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