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.
- 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 +557 -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 +388 -74
- 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/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 +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 +4 -0
- tactus/validation/semantic_visitor.py +357 -6
- tactus/validation/validator.py +142 -2
- {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/METADATA +3 -2
- {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/RECORD +46 -28
- {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
- {tactus-0.37.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
}
|
|
@@ -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
|
-
|
|
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
|