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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
local function merge_defaults(defaults, config)
|
|
2
|
+
local merged = {}
|
|
3
|
+
if defaults then
|
|
4
|
+
for key, value in pairs(defaults) do
|
|
5
|
+
merged[key] = value
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
if config then
|
|
9
|
+
for key, value in pairs(config) do
|
|
10
|
+
merged[key] = value
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
return merged
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
local corpora_base = require("tactus.corpora.base")
|
|
17
|
+
|
|
18
|
+
local function wrap_retriever(defaults)
|
|
19
|
+
local function constructor(config)
|
|
20
|
+
local merged = merge_defaults(defaults, config or {})
|
|
21
|
+
return _tactus_internal_retriever(merged)
|
|
22
|
+
end
|
|
23
|
+
local cls = {}
|
|
24
|
+
function cls:new(config)
|
|
25
|
+
return constructor(config)
|
|
26
|
+
end
|
|
27
|
+
return setmetatable(cls, {
|
|
28
|
+
__call = function(_, config)
|
|
29
|
+
return constructor(config)
|
|
30
|
+
end,
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
wrap_corpus = corpora_base.wrap_corpus,
|
|
36
|
+
wrap_retriever = wrap_retriever,
|
|
37
|
+
}
|
|
@@ -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
|
@@ -45,18 +45,18 @@ class TactusTestContext:
|
|
|
45
45
|
self.total_tokens: int = 0 # Track total tokens
|
|
46
46
|
self.cost_breakdown: List[Any] = [] # Track per-call costs
|
|
47
47
|
self._agent_mock_turns: Dict[str, List[Dict[str, Any]]] = {}
|
|
48
|
-
self._scenario_message: str
|
|
48
|
+
self._scenario_message: Optional[str] = None
|
|
49
49
|
|
|
50
50
|
def set_scenario_message(self, message: str) -> None:
|
|
51
51
|
"""Set the scenario's primary injected message (for in-spec mocking coordination)."""
|
|
52
52
|
self._scenario_message = message
|
|
53
53
|
|
|
54
|
-
def get_scenario_message(self) -> str
|
|
54
|
+
def get_scenario_message(self) -> Optional[str]:
|
|
55
55
|
"""Get the scenario's primary injected message, if set."""
|
|
56
56
|
return self._scenario_message
|
|
57
57
|
|
|
58
58
|
def mock_agent_response(
|
|
59
|
-
self, agent: str, message: str, when_message: str
|
|
59
|
+
self, agent: str, message: str, when_message: Optional[str] = None
|
|
60
60
|
) -> None:
|
|
61
61
|
"""Add a mocked agent response for this scenario (temporal; 1 per agent turn).
|
|
62
62
|
|
|
@@ -79,8 +79,8 @@ class TactusTestContext:
|
|
|
79
79
|
self,
|
|
80
80
|
agent: str,
|
|
81
81
|
tool: str,
|
|
82
|
-
args: Dict[str, Any]
|
|
83
|
-
when_message: str
|
|
82
|
+
args: Optional[Dict[str, Any]] = None,
|
|
83
|
+
when_message: Optional[str] = None,
|
|
84
84
|
) -> None:
|
|
85
85
|
"""Add a mocked tool call to an agent's next mocked turn for this scenario."""
|
|
86
86
|
args = args or {}
|
|
@@ -114,7 +114,7 @@ class TactusTestContext:
|
|
|
114
114
|
self.runtime.external_agent_mocks = self._agent_mock_turns
|
|
115
115
|
|
|
116
116
|
def mock_agent_data(
|
|
117
|
-
self, agent: str, data: Dict[str, Any], when_message: str
|
|
117
|
+
self, agent: str, data: Dict[str, Any], when_message: Optional[str] = None
|
|
118
118
|
) -> None:
|
|
119
119
|
"""Set structured output mock data for an agent's next mocked turn.
|
|
120
120
|
|
|
@@ -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
|
|
@@ -94,6 +94,9 @@ class TactusEvaluationRunner(TactusTestRunner):
|
|
|
94
94
|
EvaluationResult with all metrics
|
|
95
95
|
"""
|
|
96
96
|
logger.info(f"Evaluating scenario '{scenario_name}' with {runs} runs")
|
|
97
|
+
run_iteration = self._run_single_iteration
|
|
98
|
+
if isinstance(run_iteration, staticmethod):
|
|
99
|
+
run_iteration = run_iteration.__func__
|
|
97
100
|
|
|
98
101
|
# Run scenario N times
|
|
99
102
|
if parallel:
|
|
@@ -102,12 +105,9 @@ class TactusEvaluationRunner(TactusTestRunner):
|
|
|
102
105
|
ctx = multiprocessing.get_context("spawn")
|
|
103
106
|
with ctx.Pool(processes=workers) as pool:
|
|
104
107
|
iteration_args = [(scenario_name, str(self.work_dir), i) for i in range(runs)]
|
|
105
|
-
results = pool.starmap(
|
|
108
|
+
results = pool.starmap(run_iteration, iteration_args)
|
|
106
109
|
else:
|
|
107
|
-
results = [
|
|
108
|
-
self._run_single_iteration(scenario_name, str(self.work_dir), i)
|
|
109
|
-
for i in range(runs)
|
|
110
|
-
]
|
|
110
|
+
results = [run_iteration(scenario_name, str(self.work_dir), i) for i in range(runs)]
|
|
111
111
|
|
|
112
112
|
# Calculate metrics
|
|
113
113
|
return self._calculate_metrics(scenario_name, results)
|
tactus/testing/steps/builtin.py
CHANGED
|
@@ -14,7 +14,7 @@ Provides a comprehensive library of steps for testing:
|
|
|
14
14
|
import logging
|
|
15
15
|
import re
|
|
16
16
|
import ast
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Optional
|
|
18
18
|
|
|
19
19
|
from .registry import StepRegistry
|
|
20
20
|
|
|
@@ -645,7 +645,7 @@ def step_agent_takes_turn(context: Any, agent: str) -> None:
|
|
|
645
645
|
|
|
646
646
|
|
|
647
647
|
def step_mock_agent_responds_with(
|
|
648
|
-
context: Any, agent: str, message: str, when_message: str
|
|
648
|
+
context: Any, agent: str, message: str, when_message: Optional[str] = None
|
|
649
649
|
) -> None:
|
|
650
650
|
"""Configure a per-scenario mock agent response (temporal)."""
|
|
651
651
|
message, _ = _parse_step_string_literal(message)
|
tactus/testing/test_runner.py
CHANGED
|
@@ -128,6 +128,10 @@ class TactusTestRunner:
|
|
|
128
128
|
if not scenarios:
|
|
129
129
|
raise ValueError(f"Scenario not found: {scenario_filter}")
|
|
130
130
|
|
|
131
|
+
run_scenario = self._run_single_scenario
|
|
132
|
+
if isinstance(run_scenario, staticmethod):
|
|
133
|
+
run_scenario = run_scenario.__func__
|
|
134
|
+
|
|
131
135
|
# Run scenarios
|
|
132
136
|
if parallel and len(scenarios) > 1:
|
|
133
137
|
# Run in parallel using 'spawn' to avoid Behave global state conflicts
|
|
@@ -135,13 +139,11 @@ class TactusTestRunner:
|
|
|
135
139
|
ctx = multiprocessing.get_context("spawn")
|
|
136
140
|
with ctx.Pool(processes=min(len(scenarios), os.cpu_count() or 1)) as pool:
|
|
137
141
|
scenario_results = pool.starmap(
|
|
138
|
-
|
|
142
|
+
run_scenario, [(s.name, str(self.work_dir)) for s in scenarios]
|
|
139
143
|
)
|
|
140
144
|
else:
|
|
141
145
|
# Run sequentially
|
|
142
|
-
scenario_results = [
|
|
143
|
-
self._run_single_scenario(s.name, str(self.work_dir)) for s in scenarios
|
|
144
|
-
]
|
|
146
|
+
scenario_results = [run_scenario(s.name, str(self.work_dir)) for s in scenarios]
|
|
145
147
|
|
|
146
148
|
# Build feature result
|
|
147
149
|
feature_result = self._build_feature_result(scenario_results)
|
tactus/utils/asyncio_helpers.py
CHANGED