tactus 0.37.0__py3-none-any.whl → 0.39.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 +568 -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 +441 -75
- 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/plugins/__init__.py +3 -0
- tactus/plugins/noaa.py +76 -0
- 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 +208 -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 +430 -88
- tactus/validation/validator.py +142 -2
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/METADATA +3 -2
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/RECORD +48 -28
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/WHEEL +0 -0
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/runtime.py
CHANGED
|
@@ -13,9 +13,10 @@ import io
|
|
|
13
13
|
import logging
|
|
14
14
|
import time
|
|
15
15
|
import uuid
|
|
16
|
+
from pathlib import Path
|
|
16
17
|
from typing import Any, Dict, List, Optional
|
|
17
18
|
|
|
18
|
-
from tactus.core.registry import ProcedureRegistry, RegistryBuilder
|
|
19
|
+
from tactus.core.registry import ProcedureRegistry, RegistryBuilder, TaskDeclaration
|
|
19
20
|
from tactus.core.dsl_stubs import create_dsl_stubs, lua_table_to_dict
|
|
20
21
|
from tactus.core.template_resolver import TemplateResolver
|
|
21
22
|
from tactus.core.message_history_manager import MessageHistoryManager
|
|
@@ -137,6 +138,7 @@ class TactusRuntime:
|
|
|
137
138
|
self.openai_api_key = openai_api_key
|
|
138
139
|
self.log_handler = log_handler
|
|
139
140
|
self._injected_tool_primitive = tool_primitive
|
|
141
|
+
self.task_name: Optional[str] = None
|
|
140
142
|
self.tool_paths = tool_paths or []
|
|
141
143
|
self.recursion_depth = recursion_depth
|
|
142
144
|
self.external_config = external_config or {}
|
|
@@ -191,7 +193,11 @@ class TactusRuntime:
|
|
|
191
193
|
logger.info("TactusRuntime initialized for procedure %s", procedure_id)
|
|
192
194
|
|
|
193
195
|
async def execute(
|
|
194
|
-
self,
|
|
196
|
+
self,
|
|
197
|
+
source: str,
|
|
198
|
+
context: Optional[Dict[str, Any]] = None,
|
|
199
|
+
format: str = "yaml",
|
|
200
|
+
task_name: Optional[str] = None,
|
|
195
201
|
) -> dict[str, Any]:
|
|
196
202
|
"""
|
|
197
203
|
Execute a workflow (Lua DSL or legacy YAML format).
|
|
@@ -215,6 +221,7 @@ class TactusRuntime:
|
|
|
215
221
|
"""
|
|
216
222
|
chat_session_id = None
|
|
217
223
|
self.context = context or {} # Store context for param merging
|
|
224
|
+
self.task_name = task_name
|
|
218
225
|
|
|
219
226
|
try:
|
|
220
227
|
# 0. Setup Lua sandbox FIRST (needed for both YAML and Lua DSL)
|
|
@@ -353,10 +360,31 @@ class TactusRuntime:
|
|
|
353
360
|
raise TactusRuntimeError(
|
|
354
361
|
f"External agent mocks for '{agent_name}' must be a list of turns"
|
|
355
362
|
)
|
|
356
|
-
|
|
363
|
+
target_name = agent_name
|
|
364
|
+
if (
|
|
365
|
+
agent_name not in self.registry.agents
|
|
366
|
+
and len(self.registry.agents) == 1
|
|
367
|
+
):
|
|
368
|
+
target_name = next(iter(self.registry.agents))
|
|
369
|
+
self.registry.agent_mocks[target_name] = AgentMockConfig(
|
|
357
370
|
temporal=temporal_turns
|
|
358
371
|
)
|
|
359
372
|
|
|
373
|
+
# If agent mocks exist but use a non-matching name and there is only one agent,
|
|
374
|
+
# remap the mock to the sole agent name.
|
|
375
|
+
if self.registry and self.registry.agent_mocks and len(self.registry.agents) == 1:
|
|
376
|
+
sole_agent = next(iter(self.registry.agents))
|
|
377
|
+
if sole_agent not in self.registry.agent_mocks:
|
|
378
|
+
unmatched = [
|
|
379
|
+
name
|
|
380
|
+
for name in self.registry.agent_mocks
|
|
381
|
+
if name not in self.registry.agents
|
|
382
|
+
]
|
|
383
|
+
if len(unmatched) == 1:
|
|
384
|
+
self.registry.agent_mocks[sole_agent] = self.registry.agent_mocks.pop(
|
|
385
|
+
unmatched[0]
|
|
386
|
+
)
|
|
387
|
+
|
|
360
388
|
# If we're in mocked mode, ensure agents are mocked deterministically even if
|
|
361
389
|
# the .tac file doesn't declare `Mocks { ... }` for them.
|
|
362
390
|
if self.mock_all_agents and self.registry:
|
|
@@ -978,66 +1006,29 @@ class TactusRuntime:
|
|
|
978
1006
|
)
|
|
979
1007
|
|
|
980
1008
|
# 6. Register DSL-defined toolsets from registry (after individual tools are registered)
|
|
981
|
-
# DEBUG: Write to stderr which should show up in logs
|
|
982
|
-
import sys
|
|
983
|
-
|
|
984
|
-
sys.stderr.write("\n\n=== DSL TOOLSET REGISTRATION START ===\n")
|
|
985
|
-
sys.stderr.write(f"Has registry: {hasattr(self, 'registry')}\n")
|
|
986
|
-
if hasattr(self, "registry") and self.registry:
|
|
987
|
-
sys.stderr.write("Registry is not None: True\n")
|
|
988
|
-
sys.stderr.write(f"Registry has toolsets attr: {hasattr(self.registry, 'toolsets')}\n")
|
|
989
|
-
if hasattr(self.registry, "toolsets"):
|
|
990
|
-
sys.stderr.write(f"Registry toolsets: {list(self.registry.toolsets.keys())}\n")
|
|
991
|
-
sys.stderr.write(f"Registry toolsets count: {len(self.registry.toolsets)}\n")
|
|
992
|
-
else:
|
|
993
|
-
sys.stderr.write("Registry is None or doesn't exist\n")
|
|
994
|
-
sys.stderr.flush()
|
|
995
|
-
|
|
996
|
-
logger.info("=== DSL TOOLSET REGISTRATION START ===")
|
|
997
|
-
logger.info(f"Has registry: {hasattr(self, 'registry')}")
|
|
998
|
-
logger.info(
|
|
999
|
-
f"Registry is not None: {self.registry is not None if hasattr(self, 'registry') else False}"
|
|
1000
|
-
)
|
|
1001
|
-
if hasattr(self, "registry") and self.registry:
|
|
1002
|
-
logger.info(f"Registry has toolsets attr: {hasattr(self.registry, 'toolsets')}")
|
|
1003
|
-
if hasattr(self.registry, "toolsets"):
|
|
1004
|
-
logger.info(f"Registry toolsets: {list(self.registry.toolsets.keys())}")
|
|
1005
|
-
logger.info(f"Registry toolsets count: {len(self.registry.toolsets)}")
|
|
1006
|
-
|
|
1007
1009
|
if hasattr(self, "registry") and self.registry and hasattr(self.registry, "toolsets"):
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1010
|
+
logger.debug(
|
|
1011
|
+
"Registering %s DSL toolset(s): %s",
|
|
1012
|
+
len(self.registry.toolsets),
|
|
1013
|
+
list(self.registry.toolsets.keys()),
|
|
1014
|
+
)
|
|
1011
1015
|
for name, definition in self.registry.toolsets.items():
|
|
1012
|
-
sys.stderr.write(
|
|
1013
|
-
f"Creating DSL toolset '{name}' with config keys: {list(definition.keys())}\n"
|
|
1014
|
-
)
|
|
1015
|
-
sys.stderr.flush()
|
|
1016
|
-
logger.info(
|
|
1017
|
-
f"Creating DSL toolset '{name}' with config keys: {list(definition.keys())}"
|
|
1018
|
-
)
|
|
1019
1016
|
try:
|
|
1017
|
+
logger.debug(
|
|
1018
|
+
"Creating DSL toolset %r with config keys: %s",
|
|
1019
|
+
name,
|
|
1020
|
+
list(definition.keys()),
|
|
1021
|
+
)
|
|
1020
1022
|
toolset = await self._create_toolset_from_config(name, definition)
|
|
1021
1023
|
if toolset:
|
|
1022
1024
|
self.toolset_registry[name] = toolset
|
|
1023
|
-
sys.stderr.write(f"✓ Registered DSL-defined toolset '{name}'\n")
|
|
1024
|
-
sys.stderr.flush()
|
|
1025
|
-
logger.info(f"✓ Registered DSL-defined toolset '{name}'")
|
|
1026
1025
|
else:
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
sys.stderr.write(f"✗ Failed to create DSL toolset '{name}': {e}\n")
|
|
1032
|
-
sys.stderr.flush()
|
|
1033
|
-
logger.error(f"✗ Failed to create DSL toolset '{name}': {e}", exc_info=True)
|
|
1026
|
+
logger.error("DSL toolset %r creation returned None", name)
|
|
1027
|
+
except Exception as exc:
|
|
1028
|
+
# Toolset creation failures should not crash the whole runtime initialization.
|
|
1029
|
+
logger.error("Failed to create DSL toolset %r: %s", name, exc, exc_info=True)
|
|
1034
1030
|
else:
|
|
1035
|
-
|
|
1036
|
-
sys.stderr.flush()
|
|
1037
|
-
logger.warning("No DSL toolsets to register (registry.toolsets not available)")
|
|
1038
|
-
sys.stderr.write("=== DSL TOOLSET REGISTRATION END ===\n")
|
|
1039
|
-
sys.stderr.flush()
|
|
1040
|
-
logger.info("=== DSL TOOLSET REGISTRATION END ===")
|
|
1031
|
+
logger.debug("No DSL toolsets to register")
|
|
1041
1032
|
|
|
1042
1033
|
logger.info(
|
|
1043
1034
|
f"Toolset registry initialized with {len(self.toolset_registry)} toolset(s): {list(self.toolset_registry.keys())}"
|
|
@@ -1797,8 +1788,6 @@ class TactusRuntime:
|
|
|
1797
1788
|
Args:
|
|
1798
1789
|
context: Procedure context with pre-loaded data
|
|
1799
1790
|
"""
|
|
1800
|
-
import sys # For debug output
|
|
1801
|
-
|
|
1802
1791
|
logger.info(
|
|
1803
1792
|
f"_setup_agents called. Toolset registry has {len(self.toolset_registry)} toolsets: {list(self.toolset_registry.keys())}"
|
|
1804
1793
|
)
|
|
@@ -1980,20 +1969,14 @@ class TactusRuntime:
|
|
|
1980
1969
|
logger.info(f"Agent '{agent_name}' has NO tools (explicitly empty - passing None)")
|
|
1981
1970
|
else:
|
|
1982
1971
|
# Parse toolset expressions
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1972
|
+
logger.debug(
|
|
1973
|
+
"Agent %r raw tools config: %s (available toolsets: %s)",
|
|
1974
|
+
agent_name,
|
|
1975
|
+
agent_tools_config,
|
|
1976
|
+
list(self.toolset_registry.keys()),
|
|
1988
1977
|
)
|
|
1989
|
-
sys.stderr.flush()
|
|
1990
|
-
logger.info(f"Agent '{agent_name}' raw tools config: {agent_tools_config}")
|
|
1991
1978
|
filtered_toolsets = self._parse_toolset_expressions(agent_tools_config)
|
|
1992
|
-
|
|
1993
|
-
f"[AGENT_SETUP] Agent '{agent_name}' parsed toolsets: {filtered_toolsets}\n"
|
|
1994
|
-
)
|
|
1995
|
-
sys.stderr.flush()
|
|
1996
|
-
logger.info(f"Agent '{agent_name}' parsed toolsets: {filtered_toolsets}")
|
|
1979
|
+
logger.debug("Agent %r parsed toolsets: %s", agent_name, filtered_toolsets)
|
|
1997
1980
|
|
|
1998
1981
|
# Append inline tools toolset if present
|
|
1999
1982
|
if inline_tools_toolset:
|
|
@@ -2065,6 +2048,7 @@ class TactusRuntime:
|
|
|
2065
2048
|
"system_prompt": system_prompt_template,
|
|
2066
2049
|
"model": model_name,
|
|
2067
2050
|
"provider": agent_config.get("provider"),
|
|
2051
|
+
"context": agent_config.get("context"),
|
|
2068
2052
|
"tools": filtered_tools,
|
|
2069
2053
|
"toolsets": filtered_toolsets,
|
|
2070
2054
|
"output_schema": output_schema,
|
|
@@ -2112,10 +2096,7 @@ class TactusRuntime:
|
|
|
2112
2096
|
# The agent was created during parsing WITHOUT toolsets, now we update it
|
|
2113
2097
|
# to the new agent that HAS toolsets
|
|
2114
2098
|
self.lua_sandbox.lua.globals()[agent_name] = agent_primitive
|
|
2115
|
-
|
|
2116
|
-
f"[AGENT_FIX] Updated Lua global '{agent_name}' to new agent with toolsets\n"
|
|
2117
|
-
)
|
|
2118
|
-
sys.stderr.flush()
|
|
2099
|
+
logger.debug("Updated Lua global %r to new agent with toolsets", agent_name)
|
|
2119
2100
|
|
|
2120
2101
|
logger.info(f"Agent '{agent_name}' configured successfully with model '{model_name}'")
|
|
2121
2102
|
|
|
@@ -2516,6 +2497,60 @@ class TactusRuntime:
|
|
|
2516
2497
|
Returns:
|
|
2517
2498
|
Result from Lua procedure execution
|
|
2518
2499
|
"""
|
|
2500
|
+
if not self.task_name and self.registry:
|
|
2501
|
+
explicit_tasks = getattr(self.registry, "tasks", {}) or {}
|
|
2502
|
+
named_procedures = getattr(self.registry, "named_procedures", {}) or {}
|
|
2503
|
+
|
|
2504
|
+
def _flatten_tasks(task_map: dict, prefix: str = "") -> list[str]:
|
|
2505
|
+
names: list[str] = []
|
|
2506
|
+
for task_name, task in task_map.items():
|
|
2507
|
+
full_name = f"{prefix}{task_name}" if not prefix else f"{prefix}:{task_name}"
|
|
2508
|
+
names.append(full_name)
|
|
2509
|
+
if task.children:
|
|
2510
|
+
names.extend(_flatten_tasks(task.children, full_name))
|
|
2511
|
+
return names
|
|
2512
|
+
|
|
2513
|
+
implicit_tasks: list[str] = []
|
|
2514
|
+
retrievers = getattr(self.registry, "retrievers", {}) or {}
|
|
2515
|
+
if retrievers:
|
|
2516
|
+
from tactus.core.retriever_tasks import (
|
|
2517
|
+
resolve_retriever_id,
|
|
2518
|
+
supported_retriever_tasks,
|
|
2519
|
+
)
|
|
2520
|
+
|
|
2521
|
+
for retriever_name, retriever in retrievers.items():
|
|
2522
|
+
config = getattr(retriever, "config", {})
|
|
2523
|
+
retriever_id = resolve_retriever_id(config if isinstance(config, dict) else {})
|
|
2524
|
+
for task in sorted(supported_retriever_tasks(retriever_id)):
|
|
2525
|
+
if task in explicit_tasks:
|
|
2526
|
+
continue
|
|
2527
|
+
if task not in implicit_tasks:
|
|
2528
|
+
implicit_tasks.append(task)
|
|
2529
|
+
implicit_tasks.append(f"{task}:{retriever_name}")
|
|
2530
|
+
|
|
2531
|
+
if explicit_tasks:
|
|
2532
|
+
if "run" in explicit_tasks:
|
|
2533
|
+
self.task_name = "run"
|
|
2534
|
+
elif "main" in named_procedures:
|
|
2535
|
+
self.task_name = None
|
|
2536
|
+
elif len(explicit_tasks) == 1:
|
|
2537
|
+
self.task_name = next(iter(explicit_tasks.keys()))
|
|
2538
|
+
else:
|
|
2539
|
+
from tactus.core.exceptions import TaskSelectionRequired
|
|
2540
|
+
|
|
2541
|
+
raise TaskSelectionRequired(_flatten_tasks(explicit_tasks) + implicit_tasks)
|
|
2542
|
+
elif (
|
|
2543
|
+
implicit_tasks
|
|
2544
|
+
and not getattr(self, "_top_level_result", None)
|
|
2545
|
+
and not getattr(self.registry, "named_procedures", None)
|
|
2546
|
+
):
|
|
2547
|
+
from tactus.core.exceptions import TaskSelectionRequired
|
|
2548
|
+
|
|
2549
|
+
raise TaskSelectionRequired(implicit_tasks)
|
|
2550
|
+
|
|
2551
|
+
if self.task_name:
|
|
2552
|
+
return self._execute_task(self.task_name)
|
|
2553
|
+
|
|
2519
2554
|
if self.registry:
|
|
2520
2555
|
# Check for named 'main' procedure first
|
|
2521
2556
|
if "main" in self.registry.named_procedures:
|
|
@@ -2604,6 +2639,198 @@ class TactusRuntime:
|
|
|
2604
2639
|
logger.error(f"Legacy procedure execution failed: {e}")
|
|
2605
2640
|
raise
|
|
2606
2641
|
|
|
2642
|
+
def _execute_task(self, task_name: str) -> Any:
|
|
2643
|
+
if not self.registry:
|
|
2644
|
+
raise RuntimeError("No registry available for task execution")
|
|
2645
|
+
|
|
2646
|
+
task = self._resolve_task(task_name)
|
|
2647
|
+
if task is None:
|
|
2648
|
+
# Allow run fallback to main procedure
|
|
2649
|
+
if task_name == "run":
|
|
2650
|
+
self.task_name = None
|
|
2651
|
+
return self._execute_workflow()
|
|
2652
|
+
|
|
2653
|
+
retriever_tasks = self._resolve_retriever_task_targets(task_name)
|
|
2654
|
+
if retriever_tasks:
|
|
2655
|
+
return self._execute_retriever_tasks(task_name, retriever_tasks)
|
|
2656
|
+
|
|
2657
|
+
raise RuntimeError(f"Task '{task_name}' not found")
|
|
2658
|
+
|
|
2659
|
+
task_payload = task.model_dump()
|
|
2660
|
+
entry = task_payload.get("entry")
|
|
2661
|
+
if entry is None:
|
|
2662
|
+
if task.children:
|
|
2663
|
+
raise RuntimeError(
|
|
2664
|
+
f"Task '{task_name}' has no entry. Available sub-tasks: "
|
|
2665
|
+
f"{', '.join(task.children.keys())}"
|
|
2666
|
+
)
|
|
2667
|
+
raise RuntimeError(f"Task '{task_name}' has no entry")
|
|
2668
|
+
|
|
2669
|
+
if not callable(entry):
|
|
2670
|
+
raise RuntimeError(f"Task '{task_name}' entry must be a function")
|
|
2671
|
+
return entry()
|
|
2672
|
+
|
|
2673
|
+
def _execute_retriever_tasks(self, task_name: str, retriever_names: list[str]) -> Any:
|
|
2674
|
+
if not retriever_names:
|
|
2675
|
+
raise RuntimeError(f"No retrievers available for task '{task_name}'")
|
|
2676
|
+
|
|
2677
|
+
base_task = task_name.split(":")[0]
|
|
2678
|
+
if base_task == "index":
|
|
2679
|
+
results = []
|
|
2680
|
+
for retriever_name in retriever_names:
|
|
2681
|
+
results.append(self._execute_retriever_index(retriever_name))
|
|
2682
|
+
return results[0] if len(results) == 1 else results
|
|
2683
|
+
|
|
2684
|
+
raise RuntimeError(f"Retriever task '{base_task}' is not supported")
|
|
2685
|
+
|
|
2686
|
+
def _execute_retriever_index(self, retriever_name: str) -> Any:
|
|
2687
|
+
if not self.registry:
|
|
2688
|
+
raise RuntimeError("No registry available for retriever execution")
|
|
2689
|
+
|
|
2690
|
+
retriever = self.registry.retrievers.get(retriever_name)
|
|
2691
|
+
if retriever is None:
|
|
2692
|
+
raise RuntimeError(f"Retriever '{retriever_name}' not found")
|
|
2693
|
+
|
|
2694
|
+
if not retriever.corpus:
|
|
2695
|
+
raise RuntimeError(f"Retriever '{retriever_name}' has no corpus configured")
|
|
2696
|
+
|
|
2697
|
+
corpus_decl = self.registry.corpora.get(retriever.corpus)
|
|
2698
|
+
if corpus_decl is None:
|
|
2699
|
+
raise RuntimeError(f"Corpus '{retriever.corpus}' not found for '{retriever_name}'")
|
|
2700
|
+
|
|
2701
|
+
corpus_root = corpus_decl.config.get("corpus_root") or corpus_decl.config.get("root")
|
|
2702
|
+
if not corpus_root:
|
|
2703
|
+
raise RuntimeError(f"Corpus '{retriever.corpus}' is missing a root path")
|
|
2704
|
+
|
|
2705
|
+
extraction_pipeline = {}
|
|
2706
|
+
corpus_configuration = (
|
|
2707
|
+
corpus_decl.config.get("configuration", {})
|
|
2708
|
+
if isinstance(corpus_decl.config, dict)
|
|
2709
|
+
else {}
|
|
2710
|
+
)
|
|
2711
|
+
if isinstance(corpus_configuration, dict):
|
|
2712
|
+
pipeline = corpus_configuration.get("pipeline", {}) or {}
|
|
2713
|
+
if isinstance(pipeline, dict):
|
|
2714
|
+
extraction_pipeline = pipeline.get("extract", {}) or {}
|
|
2715
|
+
if isinstance(extraction_pipeline, list):
|
|
2716
|
+
extraction_pipeline = {}
|
|
2717
|
+
|
|
2718
|
+
retriever_id = retriever.config.get("retriever_id") or retriever.config.get(
|
|
2719
|
+
"retriever_type"
|
|
2720
|
+
)
|
|
2721
|
+
if not retriever_id:
|
|
2722
|
+
raise RuntimeError(
|
|
2723
|
+
f"Retriever '{retriever_name}' is missing retriever_id; cannot build snapshot"
|
|
2724
|
+
)
|
|
2725
|
+
|
|
2726
|
+
configuration = retriever.config.get("configuration", {})
|
|
2727
|
+
pipeline = configuration.get("pipeline", {}) if isinstance(configuration, dict) else {}
|
|
2728
|
+
if not pipeline and isinstance(retriever.config.get("pipeline"), dict):
|
|
2729
|
+
pipeline = retriever.config.get("pipeline") or {}
|
|
2730
|
+
index_config = pipeline.get("index", {}) if isinstance(pipeline, dict) else {}
|
|
2731
|
+
if isinstance(index_config, list):
|
|
2732
|
+
index_config = {}
|
|
2733
|
+
|
|
2734
|
+
try:
|
|
2735
|
+
from biblicus.corpus import Corpus
|
|
2736
|
+
from biblicus.extraction import build_extraction_snapshot
|
|
2737
|
+
from biblicus.retrievers import get_retriever
|
|
2738
|
+
except Exception as exc:
|
|
2739
|
+
raise RuntimeError(f"Biblicus retrieval retriever unavailable: {exc}") from exc
|
|
2740
|
+
|
|
2741
|
+
corpus = Corpus(Path(corpus_root))
|
|
2742
|
+
retriever_impl = get_retriever(retriever_id)
|
|
2743
|
+
configuration_name = retriever_name
|
|
2744
|
+
|
|
2745
|
+
if extraction_pipeline and isinstance(extraction_pipeline, dict):
|
|
2746
|
+
extraction_manifest = build_extraction_snapshot(
|
|
2747
|
+
corpus,
|
|
2748
|
+
extractor_id="pipeline",
|
|
2749
|
+
configuration_name=f"{retriever.corpus or retriever_name}-extract",
|
|
2750
|
+
configuration=extraction_pipeline,
|
|
2751
|
+
)
|
|
2752
|
+
if isinstance(index_config, dict) and "extraction_snapshot" not in index_config:
|
|
2753
|
+
index_config["extraction_snapshot"] = (
|
|
2754
|
+
f"{extraction_manifest.configuration.extractor_id}:"
|
|
2755
|
+
f"{extraction_manifest.snapshot_id}"
|
|
2756
|
+
)
|
|
2757
|
+
snapshot = retriever_impl.build_snapshot(
|
|
2758
|
+
corpus, configuration_name=configuration_name, configuration=index_config
|
|
2759
|
+
)
|
|
2760
|
+
|
|
2761
|
+
return snapshot.model_dump()
|
|
2762
|
+
|
|
2763
|
+
def _resolve_task(self, task_name: str) -> Optional[TaskDeclaration]:
|
|
2764
|
+
if not self.registry:
|
|
2765
|
+
return None
|
|
2766
|
+
segments = [segment for segment in task_name.split(":") if segment]
|
|
2767
|
+
if not segments:
|
|
2768
|
+
return None
|
|
2769
|
+
current = self.registry.tasks.get(segments[0])
|
|
2770
|
+
for segment in segments[1:]:
|
|
2771
|
+
if current is None:
|
|
2772
|
+
return None
|
|
2773
|
+
child = current.children.get(segment)
|
|
2774
|
+
if child is None:
|
|
2775
|
+
payload = current.model_dump(exclude={"name", "children"})
|
|
2776
|
+
inline_child = payload.get(segment)
|
|
2777
|
+
if isinstance(inline_child, dict):
|
|
2778
|
+
task_payload = dict(inline_child)
|
|
2779
|
+
task_payload["name"] = segment
|
|
2780
|
+
try:
|
|
2781
|
+
return TaskDeclaration(**task_payload)
|
|
2782
|
+
except Exception:
|
|
2783
|
+
return None
|
|
2784
|
+
return None
|
|
2785
|
+
current = child
|
|
2786
|
+
return current
|
|
2787
|
+
|
|
2788
|
+
def _resolve_retriever_task_targets(self, task_name: str) -> list[str]:
|
|
2789
|
+
if not self.registry or not self.registry.retrievers:
|
|
2790
|
+
return []
|
|
2791
|
+
segments = [segment for segment in task_name.split(":") if segment]
|
|
2792
|
+
if not segments:
|
|
2793
|
+
return []
|
|
2794
|
+
task = segments[0]
|
|
2795
|
+
target = segments[1] if len(segments) > 1 else None
|
|
2796
|
+
from tactus.core.retriever_tasks import resolve_retriever_id, supported_retriever_tasks
|
|
2797
|
+
|
|
2798
|
+
targets: list[str] = []
|
|
2799
|
+
for retriever_name, retriever in self.registry.retrievers.items():
|
|
2800
|
+
config = getattr(retriever, "config", {})
|
|
2801
|
+
retriever_id = resolve_retriever_id(config if isinstance(config, dict) else {})
|
|
2802
|
+
if task in supported_retriever_tasks(retriever_id):
|
|
2803
|
+
targets.append(retriever_name)
|
|
2804
|
+
|
|
2805
|
+
if target:
|
|
2806
|
+
return [name for name in targets if name == target]
|
|
2807
|
+
return targets
|
|
2808
|
+
|
|
2809
|
+
def _expand_inline_task_children(self, registry: ProcedureRegistry) -> None:
|
|
2810
|
+
if not registry or not registry.tasks:
|
|
2811
|
+
return
|
|
2812
|
+
|
|
2813
|
+
def add_children(parent: TaskDeclaration) -> None:
|
|
2814
|
+
payload = parent.model_dump(exclude={"name", "children"})
|
|
2815
|
+
for key, value in payload.items():
|
|
2816
|
+
if not isinstance(key, str) or not isinstance(value, dict):
|
|
2817
|
+
continue
|
|
2818
|
+
if not value.get("__tactus_task_config"):
|
|
2819
|
+
continue
|
|
2820
|
+
if key in parent.children:
|
|
2821
|
+
continue
|
|
2822
|
+
task_payload = dict(value)
|
|
2823
|
+
task_payload["name"] = key
|
|
2824
|
+
try:
|
|
2825
|
+
child = TaskDeclaration(**task_payload)
|
|
2826
|
+
except Exception:
|
|
2827
|
+
continue
|
|
2828
|
+
parent.children[key] = child
|
|
2829
|
+
add_children(child)
|
|
2830
|
+
|
|
2831
|
+
for task in registry.tasks.values():
|
|
2832
|
+
add_children(task)
|
|
2833
|
+
|
|
2607
2834
|
def _maybe_transform_script_mode_source(self, source: str) -> str:
|
|
2608
2835
|
"""
|
|
2609
2836
|
Transform "script mode" source into an implicit Procedure wrapper.
|
|
@@ -2657,6 +2884,7 @@ class TactusRuntime:
|
|
|
2657
2884
|
decl_start = re.compile(
|
|
2658
2885
|
r"^\s*(?:"
|
|
2659
2886
|
r"input|output|Mocks|Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt|"
|
|
2887
|
+
r"Task|IncludeTasks|Context|Corpus|Retriever|Compactor|"
|
|
2660
2888
|
r"Specifications|Evaluation|Evaluations|"
|
|
2661
2889
|
r"default_provider|default_model|return_prompt|error_prompt|status_prompt|async|"
|
|
2662
2890
|
r"max_depth|max_turns"
|
|
@@ -2665,7 +2893,13 @@ class TactusRuntime:
|
|
|
2665
2893
|
require_stmt = re.compile(r"^\s*(?:local\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*require\(")
|
|
2666
2894
|
assignment_decl = re.compile(
|
|
2667
2895
|
r"^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*(?:"
|
|
2668
|
-
r"Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt"
|
|
2896
|
+
r"Agent|Toolset|Tool|Model|Module|Signature|LM|Dependency|Prompt|"
|
|
2897
|
+
r"Task|TaskFunction|Context|Corpus|Retriever|Compactor|function|"
|
|
2898
|
+
r"[A-Za-z_][A-Za-z0-9_]*Retriever|"
|
|
2899
|
+
r"[A-Za-z_][A-Za-z0-9_]*\.Retriever|"
|
|
2900
|
+
r"[A-Za-z_][A-Za-z0-9_]*\.Corpus|"
|
|
2901
|
+
r"[A-Za-z_][A-Za-z0-9_]*\.Context|"
|
|
2902
|
+
r"[A-Za-z_][A-Za-z0-9_]*\.Compactor"
|
|
2669
2903
|
r")\b"
|
|
2670
2904
|
)
|
|
2671
2905
|
# Match function definitions: function name() or local function name()
|
|
@@ -2940,6 +3174,96 @@ class TactusRuntime:
|
|
|
2940
3174
|
except LuaSandboxError as e:
|
|
2941
3175
|
raise TactusRuntimeError(f"Failed to parse DSL: {e}")
|
|
2942
3176
|
|
|
3177
|
+
lua_globals = sandbox.lua.globals()
|
|
3178
|
+
self._register_assignment_tasks(builder, lua_globals)
|
|
3179
|
+
|
|
3180
|
+
self._expand_inline_task_children(builder.registry)
|
|
3181
|
+
|
|
3182
|
+
# Execute IncludeTasks files to register additional tasks
|
|
3183
|
+
if builder.registry.include_tasks:
|
|
3184
|
+
base_path = Path(self.source_file_path).parent if self.source_file_path else Path.cwd()
|
|
3185
|
+
include_queue = [
|
|
3186
|
+
{
|
|
3187
|
+
"path": include.get("path"),
|
|
3188
|
+
"namespace": include.get("namespace"),
|
|
3189
|
+
"base": base_path,
|
|
3190
|
+
}
|
|
3191
|
+
for include in builder.registry.include_tasks
|
|
3192
|
+
]
|
|
3193
|
+
seen_includes: set[Path] = set()
|
|
3194
|
+
|
|
3195
|
+
while include_queue:
|
|
3196
|
+
include = include_queue.pop(0)
|
|
3197
|
+
include_path = include.get("path")
|
|
3198
|
+
if not include_path:
|
|
3199
|
+
continue
|
|
3200
|
+
include_base = include.get("base") or base_path
|
|
3201
|
+
include_file = (include_base / include_path).resolve()
|
|
3202
|
+
if include_file in seen_includes:
|
|
3203
|
+
raise TactusRuntimeError(f"IncludeTasks cycle detected: {include_file}")
|
|
3204
|
+
seen_includes.add(include_file)
|
|
3205
|
+
if not include_file.exists():
|
|
3206
|
+
raise TactusRuntimeError(f"Included tasks file not found: {include_file}")
|
|
3207
|
+
|
|
3208
|
+
pre_task_names = set(builder.registry.tasks.keys())
|
|
3209
|
+
pre_include_count = len(builder.registry.include_tasks)
|
|
3210
|
+
pre_counts = {
|
|
3211
|
+
"agents": len(builder.registry.agents),
|
|
3212
|
+
"toolsets": len(builder.registry.toolsets),
|
|
3213
|
+
"lua_tools": len(builder.registry.lua_tools),
|
|
3214
|
+
"contexts": len(builder.registry.contexts),
|
|
3215
|
+
"corpora": len(builder.registry.corpora),
|
|
3216
|
+
"retrievers": len(builder.registry.retrievers),
|
|
3217
|
+
"compactors": len(builder.registry.compactors),
|
|
3218
|
+
"named_procedures": len(builder.registry.named_procedures),
|
|
3219
|
+
}
|
|
3220
|
+
include_source = include_file.read_text()
|
|
3221
|
+
try:
|
|
3222
|
+
sandbox.execute(include_source)
|
|
3223
|
+
except LuaSandboxError as e:
|
|
3224
|
+
raise TactusRuntimeError(f"Failed to execute IncludeTasks file: {e}")
|
|
3225
|
+
|
|
3226
|
+
post_counts = {
|
|
3227
|
+
"agents": len(builder.registry.agents),
|
|
3228
|
+
"toolsets": len(builder.registry.toolsets),
|
|
3229
|
+
"lua_tools": len(builder.registry.lua_tools),
|
|
3230
|
+
"contexts": len(builder.registry.contexts),
|
|
3231
|
+
"corpora": len(builder.registry.corpora),
|
|
3232
|
+
"retrievers": len(builder.registry.retrievers),
|
|
3233
|
+
"compactors": len(builder.registry.compactors),
|
|
3234
|
+
"named_procedures": len(builder.registry.named_procedures),
|
|
3235
|
+
}
|
|
3236
|
+
if any(post_counts[key] != pre_counts[key] for key in post_counts):
|
|
3237
|
+
raise TactusRuntimeError(
|
|
3238
|
+
f"IncludeTasks files must only contain Task declarations: {include_file}"
|
|
3239
|
+
)
|
|
3240
|
+
|
|
3241
|
+
new_task_names = set(builder.registry.tasks.keys()) - pre_task_names
|
|
3242
|
+
namespace = include.get("namespace")
|
|
3243
|
+
if namespace and new_task_names:
|
|
3244
|
+
if namespace in builder.registry.tasks:
|
|
3245
|
+
raise TactusRuntimeError(f"Duplicate task namespace '{namespace}'")
|
|
3246
|
+
namespaced_children = {
|
|
3247
|
+
name: builder.registry.tasks.pop(name) for name in new_task_names
|
|
3248
|
+
}
|
|
3249
|
+
builder.registry.tasks[namespace] = TaskDeclaration(
|
|
3250
|
+
name=namespace,
|
|
3251
|
+
children=namespaced_children,
|
|
3252
|
+
)
|
|
3253
|
+
|
|
3254
|
+
new_includes = builder.registry.include_tasks[pre_include_count:]
|
|
3255
|
+
if new_includes:
|
|
3256
|
+
include_queue.extend(
|
|
3257
|
+
[
|
|
3258
|
+
{
|
|
3259
|
+
"path": nested.get("path"),
|
|
3260
|
+
"namespace": nested.get("namespace"),
|
|
3261
|
+
"base": include_file.parent,
|
|
3262
|
+
}
|
|
3263
|
+
for nested in new_includes
|
|
3264
|
+
]
|
|
3265
|
+
)
|
|
3266
|
+
|
|
2943
3267
|
# Auto-register plain function main() if it exists
|
|
2944
3268
|
#
|
|
2945
3269
|
# Some .tac files use plain Lua syntax: `function main() ... end`
|
|
@@ -2955,7 +3279,6 @@ class TactusRuntime:
|
|
|
2955
3279
|
# The script mode transformation (in _maybe_transform_script_mode_source)
|
|
2956
3280
|
# is designed to skip files with named function definitions to avoid wrapping
|
|
2957
3281
|
# them incorrectly.
|
|
2958
|
-
lua_globals = sandbox.lua.globals()
|
|
2959
3282
|
if "main" in lua_globals:
|
|
2960
3283
|
main_func = lua_globals["main"]
|
|
2961
3284
|
# Check if it's a function and not already registered
|
|
@@ -2983,6 +3306,49 @@ class TactusRuntime:
|
|
|
2983
3306
|
logger.debug(f"Registry after parsing: lua_tools={list(result.registry.lua_tools.keys())}")
|
|
2984
3307
|
return result.registry
|
|
2985
3308
|
|
|
3309
|
+
def _register_assignment_tasks(self, builder: RegistryBuilder, lua_globals: Any) -> None:
|
|
3310
|
+
if not lua_globals:
|
|
3311
|
+
return
|
|
3312
|
+
|
|
3313
|
+
for key, value in lua_globals.items():
|
|
3314
|
+
if not isinstance(key, str):
|
|
3315
|
+
continue
|
|
3316
|
+
if not hasattr(value, "items"):
|
|
3317
|
+
continue
|
|
3318
|
+
try:
|
|
3319
|
+
marker = value["__tactus_task_config"]
|
|
3320
|
+
except Exception:
|
|
3321
|
+
continue
|
|
3322
|
+
if not marker:
|
|
3323
|
+
continue
|
|
3324
|
+
task_config = lua_table_to_dict(value)
|
|
3325
|
+
if not isinstance(task_config, dict):
|
|
3326
|
+
continue
|
|
3327
|
+
if key in builder.registry.tasks:
|
|
3328
|
+
continue
|
|
3329
|
+
if "entry" in task_config and not callable(task_config["entry"]):
|
|
3330
|
+
raise TactusRuntimeError(f"Task '{key}' entry must be a function")
|
|
3331
|
+
builder.register_task(key, task_config)
|
|
3332
|
+
|
|
3333
|
+
child_sources = task_config.get("__tactus_child_tasks")
|
|
3334
|
+
if isinstance(child_sources, dict):
|
|
3335
|
+
child_iter = child_sources.items()
|
|
3336
|
+
else:
|
|
3337
|
+
child_iter = value.items()
|
|
3338
|
+
for child_key, child_value in child_iter:
|
|
3339
|
+
child_name = child_key if isinstance(child_key, str) else None
|
|
3340
|
+
if not hasattr(child_value, "items"):
|
|
3341
|
+
continue
|
|
3342
|
+
child_config = lua_table_to_dict(child_value)
|
|
3343
|
+
if not child_name and isinstance(child_config, dict):
|
|
3344
|
+
child_name = child_config.get("__task_name")
|
|
3345
|
+
if child_name and isinstance(child_config, dict):
|
|
3346
|
+
if "entry" in child_config and not callable(child_config["entry"]):
|
|
3347
|
+
raise TactusRuntimeError(
|
|
3348
|
+
f"Task '{key}:{child_name}' entry must be a function"
|
|
3349
|
+
)
|
|
3350
|
+
builder.register_task(child_name, child_config, parent=key)
|
|
3351
|
+
|
|
2986
3352
|
def _registry_to_config(self, registry: ProcedureRegistry) -> dict[str, Any]:
|
|
2987
3353
|
"""
|
|
2988
3354
|
Convert registry to config dict format.
|