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.
Files changed (65) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/channels/base.py +22 -2
  3. tactus/adapters/channels/broker.py +1 -0
  4. tactus/adapters/channels/host.py +3 -1
  5. tactus/adapters/channels/ipc.py +18 -3
  6. tactus/adapters/channels/sse.py +2 -0
  7. tactus/adapters/mcp_manager.py +24 -7
  8. tactus/backends/http_backend.py +2 -2
  9. tactus/backends/pytorch_backend.py +2 -2
  10. tactus/broker/client.py +3 -3
  11. tactus/broker/server.py +17 -5
  12. tactus/cli/app.py +212 -57
  13. tactus/core/compaction.py +17 -0
  14. tactus/core/context_assembler.py +73 -0
  15. tactus/core/context_models.py +41 -0
  16. tactus/core/dsl_stubs.py +560 -20
  17. tactus/core/exceptions.py +8 -0
  18. tactus/core/execution_context.py +24 -24
  19. tactus/core/message_history_manager.py +2 -2
  20. tactus/core/mocking.py +12 -0
  21. tactus/core/output_validator.py +6 -6
  22. tactus/core/registry.py +171 -29
  23. tactus/core/retrieval.py +317 -0
  24. tactus/core/retriever_tasks.py +30 -0
  25. tactus/core/runtime.py +431 -117
  26. tactus/dspy/agent.py +143 -82
  27. tactus/dspy/broker_lm.py +13 -7
  28. tactus/dspy/config.py +23 -4
  29. tactus/dspy/module.py +12 -1
  30. tactus/ide/coding_assistant.py +2 -2
  31. tactus/primitives/handles.py +79 -7
  32. tactus/primitives/model.py +1 -1
  33. tactus/primitives/procedure.py +1 -1
  34. tactus/primitives/state.py +2 -2
  35. tactus/sandbox/config.py +1 -1
  36. tactus/sandbox/container_runner.py +13 -6
  37. tactus/sandbox/entrypoint.py +51 -8
  38. tactus/sandbox/protocol.py +5 -0
  39. tactus/stdlib/README.md +10 -1
  40. tactus/stdlib/biblicus/__init__.py +3 -0
  41. tactus/stdlib/biblicus/text.py +189 -0
  42. tactus/stdlib/tac/biblicus/text.tac +32 -0
  43. tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
  44. tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
  45. tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
  46. tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
  47. tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
  48. tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
  49. tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
  50. tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
  51. tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
  52. tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
  53. tactus/testing/behave_integration.py +2 -0
  54. tactus/testing/context.py +10 -6
  55. tactus/testing/evaluation_runner.py +5 -5
  56. tactus/testing/steps/builtin.py +2 -2
  57. tactus/testing/test_runner.py +6 -4
  58. tactus/utils/asyncio_helpers.py +2 -1
  59. tactus/validation/semantic_visitor.py +357 -6
  60. tactus/validation/validator.py +142 -2
  61. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/METADATA +9 -6
  62. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/RECORD +65 -47
  63. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
  64. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
  65. {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,5 @@
1
+ local base = require("tactus.corpora.base")
2
+
3
+ return {
4
+ Corpus = base.wrap_corpus({}),
5
+ }
@@ -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,6 @@
1
+ local base = require("tactus.retrievers.base")
2
+
3
+ return {
4
+ Corpus = base.wrap_corpus({}),
5
+ Retriever = base.wrap_retriever({ retriever_id = "embedding-index-file" }),
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 = "embedding-index-inmemory" }),
6
+ }
@@ -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
@@ -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 | None = None
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 | None:
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 | None = None
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] | None = None,
83
- when_message: str | None = None,
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 | None = None
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(self._run_single_iteration, iteration_args)
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)
@@ -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 | None = None
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)
@@ -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
- self._run_single_scenario, [(s.name, str(self.work_dir)) for s in scenarios]
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)
@@ -24,4 +24,5 @@ def clear_closed_event_loop() -> None:
24
24
  return
25
25
 
26
26
  if getattr(current_loop, "is_closed", lambda: False)():
27
- asyncio.set_event_loop(None)
27
+ replacement_loop = asyncio.new_event_loop()
28
+ asyncio.set_event_loop(replacement_loop)