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
tactus/core/dsl_stubs.py
CHANGED
|
@@ -34,7 +34,16 @@ Agent/Tool calls use direct variable access:
|
|
|
34
34
|
from typing import Any, Callable, Dict, Optional
|
|
35
35
|
|
|
36
36
|
from .registry import RegistryBuilder
|
|
37
|
-
from tactus.primitives.handles import
|
|
37
|
+
from tactus.primitives.handles import (
|
|
38
|
+
AgentHandle,
|
|
39
|
+
AgentLookup,
|
|
40
|
+
CompactorHandle,
|
|
41
|
+
ContextHandle,
|
|
42
|
+
CorpusHandle,
|
|
43
|
+
ModelHandle,
|
|
44
|
+
ModelLookup,
|
|
45
|
+
RetrieverHandle,
|
|
46
|
+
)
|
|
38
47
|
from tactus.stdlib.classify import ClassifyPrimitive
|
|
39
48
|
|
|
40
49
|
|
|
@@ -108,6 +117,30 @@ def _normalize_schema(schema):
|
|
|
108
117
|
return schema
|
|
109
118
|
|
|
110
119
|
|
|
120
|
+
def _normalize_handle_name(value: Any) -> Any:
|
|
121
|
+
"""Normalize handle references to their names when possible."""
|
|
122
|
+
if hasattr(value, "name"):
|
|
123
|
+
return value.name
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _normalize_context_pack_entry(entry: Any) -> dict[str, Any]:
|
|
128
|
+
"""Normalize Context pack entries into a name-based dict."""
|
|
129
|
+
if isinstance(entry, dict):
|
|
130
|
+
normalized = dict(entry)
|
|
131
|
+
if "name" in normalized:
|
|
132
|
+
normalized["name"] = _normalize_handle_name(normalized["name"])
|
|
133
|
+
return normalized
|
|
134
|
+
return {"name": _normalize_handle_name(entry)}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _normalize_template_vars(vars_dict: Any) -> Any:
|
|
138
|
+
"""Normalize template vars coming from Lua tables."""
|
|
139
|
+
if isinstance(vars_dict, list) and len(vars_dict) == 0:
|
|
140
|
+
return {}
|
|
141
|
+
return vars_dict
|
|
142
|
+
|
|
143
|
+
|
|
111
144
|
def create_dsl_stubs(
|
|
112
145
|
builder: RegistryBuilder,
|
|
113
146
|
tool_primitive: Any = None,
|
|
@@ -136,6 +169,10 @@ def create_dsl_stubs(
|
|
|
136
169
|
_agent_registry: dict[str, AgentHandle] = {}
|
|
137
170
|
_tool_registry: dict[str, Any] = {} # ToolHandle instances
|
|
138
171
|
_model_registry: dict[str, ModelHandle] = {}
|
|
172
|
+
_context_registry: dict[str, ContextHandle] = {}
|
|
173
|
+
_corpus_registry: dict[str, CorpusHandle] = {}
|
|
174
|
+
_retriever_registry: dict[str, RetrieverHandle] = {}
|
|
175
|
+
_compactor_registry: dict[str, CompactorHandle] = {}
|
|
139
176
|
|
|
140
177
|
# Store runtime context for immediate agent creation
|
|
141
178
|
_runtime_context = runtime_context or {}
|
|
@@ -353,12 +390,89 @@ def create_dsl_stubs(
|
|
|
353
390
|
_procedure_registry[name] = stub # Store stub temporarily
|
|
354
391
|
return stub
|
|
355
392
|
|
|
356
|
-
|
|
357
|
-
def accept_config(config):
|
|
358
|
-
"""Accept config (with function as last unnamed element) and register procedure."""
|
|
359
|
-
return _process_procedure_config(name, config, _procedure_registry)
|
|
393
|
+
if name is not None:
|
|
360
394
|
|
|
361
|
-
|
|
395
|
+
def _curried(config_table=None):
|
|
396
|
+
if config_table is None or not hasattr(config_table, "items"):
|
|
397
|
+
raise TypeError(
|
|
398
|
+
f"procedure '{name}' requires a configuration table. "
|
|
399
|
+
'Use: Procedure "name" { ... } or name = Procedure { ... }.'
|
|
400
|
+
)
|
|
401
|
+
return _process_procedure_config(name, config_table, _procedure_registry)
|
|
402
|
+
|
|
403
|
+
return _curried
|
|
404
|
+
|
|
405
|
+
def _task(name_or_config=None, config=None):
|
|
406
|
+
"""
|
|
407
|
+
Task declaration stub.
|
|
408
|
+
|
|
409
|
+
Supports:
|
|
410
|
+
Task "name" { ... }
|
|
411
|
+
name = Task { ... } (name not available at runtime)
|
|
412
|
+
"""
|
|
413
|
+
# Task "name" { ... }
|
|
414
|
+
if isinstance(name_or_config, str):
|
|
415
|
+
task_name = name_or_config
|
|
416
|
+
task_config = config or {}
|
|
417
|
+
if hasattr(task_config, "__setitem__"):
|
|
418
|
+
try:
|
|
419
|
+
task_config["__task_name"] = task_name
|
|
420
|
+
task_config["__tactus_task_config"] = True
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
task_config_dict = lua_table_to_dict(task_config)
|
|
424
|
+
if "entry" in task_config_dict and not callable(task_config_dict["entry"]):
|
|
425
|
+
raise TypeError(f"Task '{task_name}' entry must be a function")
|
|
426
|
+
builder.register_task(task_name, task_config_dict)
|
|
427
|
+
|
|
428
|
+
if hasattr(task_config, "items"):
|
|
429
|
+
for key, value in task_config.items():
|
|
430
|
+
child_name = None
|
|
431
|
+
if isinstance(key, str):
|
|
432
|
+
child_name = key
|
|
433
|
+
if not hasattr(value, "items"):
|
|
434
|
+
continue
|
|
435
|
+
child_config = lua_table_to_dict(value)
|
|
436
|
+
if not child_name and isinstance(child_config, dict):
|
|
437
|
+
child_name = child_config.get("__task_name")
|
|
438
|
+
if child_name and isinstance(child_config, dict) and "entry" in child_config:
|
|
439
|
+
builder.register_task(child_name, child_config, parent=task_name)
|
|
440
|
+
return task_config
|
|
441
|
+
|
|
442
|
+
# Assignment-based: name = Task { ... }
|
|
443
|
+
if name_or_config is not None and hasattr(name_or_config, "items"):
|
|
444
|
+
task_config = name_or_config
|
|
445
|
+
if hasattr(task_config, "__setitem__"):
|
|
446
|
+
try:
|
|
447
|
+
task_config["__tactus_task_config"] = True
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
if hasattr(task_config, "items"):
|
|
451
|
+
child_tasks = {}
|
|
452
|
+
for key, value in task_config.items():
|
|
453
|
+
if isinstance(key, str) and hasattr(value, "items"):
|
|
454
|
+
child_tasks[key] = value
|
|
455
|
+
if child_tasks:
|
|
456
|
+
try:
|
|
457
|
+
task_config["__tactus_child_tasks"] = child_tasks
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
return task_config
|
|
461
|
+
|
|
462
|
+
return {}
|
|
463
|
+
|
|
464
|
+
def _include_tasks(path=None, namespace=None):
|
|
465
|
+
"""IncludeTasks stub (records include for runtime)."""
|
|
466
|
+
if hasattr(path, "items"):
|
|
467
|
+
include_config = lua_table_to_dict(path)
|
|
468
|
+
include_path = include_config.get("path")
|
|
469
|
+
include_namespace = include_config.get("namespace")
|
|
470
|
+
if isinstance(include_path, str):
|
|
471
|
+
builder.register_include_tasks(include_path, include_namespace)
|
|
472
|
+
return None
|
|
473
|
+
if isinstance(path, str):
|
|
474
|
+
builder.register_include_tasks(path, namespace if isinstance(namespace, str) else None)
|
|
475
|
+
return None
|
|
362
476
|
|
|
363
477
|
def _prompt(prompt_name: str, content: str) -> None:
|
|
364
478
|
"""Register a prompt template."""
|
|
@@ -477,6 +591,67 @@ def create_dsl_stubs(
|
|
|
477
591
|
|
|
478
592
|
return accept_config
|
|
479
593
|
|
|
594
|
+
def _template(template_text: str, vars_table: Any = None) -> dict[str, Any]:
|
|
595
|
+
"""
|
|
596
|
+
Create a template directive for context messages.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
template_text: Template string with {input.*} or {context.*} markers
|
|
600
|
+
vars_table: Optional vars table for template values
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Dict describing the template directive
|
|
604
|
+
"""
|
|
605
|
+
if not isinstance(template_text, str):
|
|
606
|
+
raise TypeError("template() expects a string")
|
|
607
|
+
vars_dict = lua_table_to_dict(vars_table) if vars_table is not None else {}
|
|
608
|
+
vars_dict = _normalize_template_vars(vars_dict)
|
|
609
|
+
return {"template": template_text, "vars": vars_dict}
|
|
610
|
+
|
|
611
|
+
def _message_from_arg(message_type: str, arg: Any) -> dict[str, Any]:
|
|
612
|
+
"""Create a message directive from a literal or template directive."""
|
|
613
|
+
if isinstance(arg, dict) and "template" in arg:
|
|
614
|
+
return {
|
|
615
|
+
"type": message_type,
|
|
616
|
+
"template": arg.get("template"),
|
|
617
|
+
"vars": arg.get("vars", {}),
|
|
618
|
+
}
|
|
619
|
+
if isinstance(arg, str):
|
|
620
|
+
return {"type": message_type, "content": arg}
|
|
621
|
+
raise TypeError(f"{message_type}() expects a string or template(...) directive")
|
|
622
|
+
|
|
623
|
+
def _system(arg: Any) -> dict[str, Any]:
|
|
624
|
+
"""System message directive for Context messages."""
|
|
625
|
+
return _message_from_arg("system", arg)
|
|
626
|
+
|
|
627
|
+
def _user(arg: Any) -> dict[str, Any]:
|
|
628
|
+
"""User message directive for Context messages."""
|
|
629
|
+
return _message_from_arg("user", arg)
|
|
630
|
+
|
|
631
|
+
def _assistant(arg: Any) -> dict[str, Any]:
|
|
632
|
+
"""Assistant message directive for Context messages."""
|
|
633
|
+
return _message_from_arg("assistant", arg)
|
|
634
|
+
|
|
635
|
+
def _context_insert(pack: Any, budget: Any = None) -> dict[str, Any]:
|
|
636
|
+
"""Context pack insertion directive for Context messages."""
|
|
637
|
+
if hasattr(pack, "name"):
|
|
638
|
+
pack_name = pack.name
|
|
639
|
+
else:
|
|
640
|
+
pack_name = pack
|
|
641
|
+
if not isinstance(pack_name, str):
|
|
642
|
+
raise TypeError("context() expects a context or retriever reference")
|
|
643
|
+
budget_dict = lua_table_to_dict(budget) if budget is not None else None
|
|
644
|
+
if isinstance(budget_dict, list) and len(budget_dict) == 0:
|
|
645
|
+
budget_dict = None
|
|
646
|
+
directive = {"type": "context", "name": pack_name}
|
|
647
|
+
if budget_dict is not None:
|
|
648
|
+
directive["budget"] = budget_dict
|
|
649
|
+
return directive
|
|
650
|
+
|
|
651
|
+
def _context_history() -> dict[str, Any]:
|
|
652
|
+
"""History insertion directive for Context messages."""
|
|
653
|
+
return {"type": "history"}
|
|
654
|
+
|
|
480
655
|
def _specification(*args) -> None:
|
|
481
656
|
"""Register BDD specs.
|
|
482
657
|
|
|
@@ -1044,7 +1219,7 @@ def create_dsl_stubs(
|
|
|
1044
1219
|
# Otherwise, ignore unknown mock config.
|
|
1045
1220
|
continue
|
|
1046
1221
|
|
|
1047
|
-
def
|
|
1222
|
+
def _dspy_history(messages=None):
|
|
1048
1223
|
"""
|
|
1049
1224
|
Create a History for managing conversation messages.
|
|
1050
1225
|
|
|
@@ -1666,6 +1841,214 @@ def create_dsl_stubs(
|
|
|
1666
1841
|
|
|
1667
1842
|
return handle
|
|
1668
1843
|
|
|
1844
|
+
def _new_context(name_or_config=None) -> ContextHandle:
|
|
1845
|
+
"""
|
|
1846
|
+
New Context factory for assignment-based syntax.
|
|
1847
|
+
|
|
1848
|
+
Syntax:
|
|
1849
|
+
support_context = Context {
|
|
1850
|
+
policy = { ... },
|
|
1851
|
+
messages = { ... }
|
|
1852
|
+
}
|
|
1853
|
+
"""
|
|
1854
|
+
if isinstance(name_or_config, str):
|
|
1855
|
+
raise TypeError(
|
|
1856
|
+
"Curried Context syntax is not supported. Use assignment syntax: "
|
|
1857
|
+
"my_context = Context { ... }."
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
config = name_or_config
|
|
1861
|
+
if config is None:
|
|
1862
|
+
raise TypeError("Context requires a configuration table")
|
|
1863
|
+
|
|
1864
|
+
config_dict = lua_table_to_dict(config)
|
|
1865
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
1866
|
+
config_dict = {}
|
|
1867
|
+
|
|
1868
|
+
explicit_name = None
|
|
1869
|
+
if isinstance(config_dict, dict):
|
|
1870
|
+
explicit_name = config_dict.pop("name", None)
|
|
1871
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
1872
|
+
raise TypeError("Context 'name' must be a string")
|
|
1873
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
1874
|
+
raise TypeError("Context 'name' cannot be empty")
|
|
1875
|
+
|
|
1876
|
+
packs = config_dict.get("packs")
|
|
1877
|
+
|
|
1878
|
+
if isinstance(packs, list):
|
|
1879
|
+
config_dict["packs"] = [_normalize_context_pack_entry(pack) for pack in packs]
|
|
1880
|
+
elif packs is not None:
|
|
1881
|
+
config_dict["packs"] = [_normalize_context_pack_entry(packs)]
|
|
1882
|
+
|
|
1883
|
+
policy = config_dict.get("policy")
|
|
1884
|
+
if isinstance(policy, dict):
|
|
1885
|
+
compactor_value = policy.get("compactor")
|
|
1886
|
+
if hasattr(compactor_value, "name"):
|
|
1887
|
+
policy["compactor"] = compactor_value.name
|
|
1888
|
+
|
|
1889
|
+
import uuid
|
|
1890
|
+
|
|
1891
|
+
temporary_name = (
|
|
1892
|
+
explicit_name.strip()
|
|
1893
|
+
if isinstance(explicit_name, str)
|
|
1894
|
+
else f"_temp_context_{uuid.uuid4().hex[:8]}"
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
builder.register_context(temporary_name, config_dict)
|
|
1898
|
+
handle = ContextHandle(temporary_name)
|
|
1899
|
+
_context_registry[temporary_name] = handle
|
|
1900
|
+
return handle
|
|
1901
|
+
|
|
1902
|
+
def _new_corpus(name_or_config=None) -> CorpusHandle:
|
|
1903
|
+
"""
|
|
1904
|
+
New Corpus factory for assignment-based syntax.
|
|
1905
|
+
"""
|
|
1906
|
+
if isinstance(name_or_config, str):
|
|
1907
|
+
raise TypeError(
|
|
1908
|
+
"Curried Corpus syntax is not supported. Use assignment syntax: "
|
|
1909
|
+
"my_corpus = Corpus { ... }."
|
|
1910
|
+
)
|
|
1911
|
+
|
|
1912
|
+
config = name_or_config
|
|
1913
|
+
if config is None:
|
|
1914
|
+
raise TypeError("Corpus requires a configuration table")
|
|
1915
|
+
|
|
1916
|
+
config_dict = lua_table_to_dict(config)
|
|
1917
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
1918
|
+
config_dict = {}
|
|
1919
|
+
|
|
1920
|
+
if isinstance(config_dict, dict):
|
|
1921
|
+
if "root" in config_dict and "corpus_root" not in config_dict:
|
|
1922
|
+
config_dict["corpus_root"] = config_dict.pop("root")
|
|
1923
|
+
|
|
1924
|
+
if isinstance(config_dict, dict):
|
|
1925
|
+
configuration = config_dict.get("configuration")
|
|
1926
|
+
if isinstance(configuration, dict):
|
|
1927
|
+
pipeline = configuration.get("pipeline")
|
|
1928
|
+
if isinstance(pipeline, list) and len(pipeline) == 0:
|
|
1929
|
+
configuration["pipeline"] = {}
|
|
1930
|
+
|
|
1931
|
+
explicit_name = None
|
|
1932
|
+
if isinstance(config_dict, dict):
|
|
1933
|
+
explicit_name = config_dict.pop("name", None)
|
|
1934
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
1935
|
+
raise TypeError("Corpus 'name' must be a string")
|
|
1936
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
1937
|
+
raise TypeError("Corpus 'name' cannot be empty")
|
|
1938
|
+
|
|
1939
|
+
import uuid
|
|
1940
|
+
|
|
1941
|
+
temporary_name = (
|
|
1942
|
+
explicit_name.strip()
|
|
1943
|
+
if isinstance(explicit_name, str)
|
|
1944
|
+
else f"_temp_corpus_{uuid.uuid4().hex[:8]}"
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1947
|
+
builder.register_corpus(temporary_name, config_dict)
|
|
1948
|
+
handle = CorpusHandle(temporary_name)
|
|
1949
|
+
_corpus_registry[temporary_name] = handle
|
|
1950
|
+
return handle
|
|
1951
|
+
|
|
1952
|
+
def _new_retriever(name_or_config=None) -> RetrieverHandle:
|
|
1953
|
+
"""
|
|
1954
|
+
New Retriever factory for assignment-based syntax.
|
|
1955
|
+
"""
|
|
1956
|
+
if isinstance(name_or_config, str):
|
|
1957
|
+
raise TypeError(
|
|
1958
|
+
"Curried Retriever syntax is not supported. Use assignment syntax: "
|
|
1959
|
+
"my_retriever = Retriever { ... }."
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
config = name_or_config
|
|
1963
|
+
if config is None:
|
|
1964
|
+
raise TypeError("Retriever requires a configuration table")
|
|
1965
|
+
|
|
1966
|
+
config_dict = lua_table_to_dict(config)
|
|
1967
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
1968
|
+
config_dict = {}
|
|
1969
|
+
|
|
1970
|
+
if isinstance(config_dict, dict):
|
|
1971
|
+
config_dict["corpus"] = _normalize_handle_name(config_dict.get("corpus"))
|
|
1972
|
+
|
|
1973
|
+
explicit_name = None
|
|
1974
|
+
if isinstance(config_dict, dict):
|
|
1975
|
+
explicit_name = config_dict.pop("name", None)
|
|
1976
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
1977
|
+
raise TypeError("Retriever 'name' must be a string")
|
|
1978
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
1979
|
+
raise TypeError("Retriever 'name' cannot be empty")
|
|
1980
|
+
|
|
1981
|
+
import uuid
|
|
1982
|
+
|
|
1983
|
+
temporary_name = (
|
|
1984
|
+
explicit_name.strip()
|
|
1985
|
+
if isinstance(explicit_name, str)
|
|
1986
|
+
else f"_temp_retriever_{uuid.uuid4().hex[:8]}"
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
builder.register_retriever(temporary_name, config_dict)
|
|
1990
|
+
handle = RetrieverHandle(temporary_name)
|
|
1991
|
+
_retriever_registry[temporary_name] = handle
|
|
1992
|
+
return handle
|
|
1993
|
+
|
|
1994
|
+
def _task_function(function):
|
|
1995
|
+
"""
|
|
1996
|
+
Wrap a callable so Task entries can use call-style syntax without executing immediately.
|
|
1997
|
+
"""
|
|
1998
|
+
if not callable(function):
|
|
1999
|
+
raise TypeError("TaskFunction expects a callable")
|
|
2000
|
+
|
|
2001
|
+
def _deferred(args):
|
|
2002
|
+
def _runner():
|
|
2003
|
+
if hasattr(args, "items"):
|
|
2004
|
+
args_dict = lua_table_to_dict(args)
|
|
2005
|
+
else:
|
|
2006
|
+
args_dict = args
|
|
2007
|
+
return function(args_dict)
|
|
2008
|
+
|
|
2009
|
+
return _runner
|
|
2010
|
+
|
|
2011
|
+
return _deferred
|
|
2012
|
+
|
|
2013
|
+
def _new_compactor(name_or_config=None) -> CompactorHandle:
|
|
2014
|
+
"""
|
|
2015
|
+
New Compactor factory for assignment-based syntax.
|
|
2016
|
+
"""
|
|
2017
|
+
if isinstance(name_or_config, str):
|
|
2018
|
+
raise TypeError(
|
|
2019
|
+
"Curried Compactor syntax is not supported. Use assignment syntax: "
|
|
2020
|
+
"my_compactor = Compactor { ... }."
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
config = name_or_config
|
|
2024
|
+
if config is None:
|
|
2025
|
+
raise TypeError("Compactor requires a configuration table")
|
|
2026
|
+
|
|
2027
|
+
config_dict = lua_table_to_dict(config)
|
|
2028
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
2029
|
+
config_dict = {}
|
|
2030
|
+
|
|
2031
|
+
explicit_name = None
|
|
2032
|
+
if isinstance(config_dict, dict):
|
|
2033
|
+
explicit_name = config_dict.pop("name", None)
|
|
2034
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
2035
|
+
raise TypeError("Compactor 'name' must be a string")
|
|
2036
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
2037
|
+
raise TypeError("Compactor 'name' cannot be empty")
|
|
2038
|
+
|
|
2039
|
+
import uuid
|
|
2040
|
+
|
|
2041
|
+
temporary_name = (
|
|
2042
|
+
explicit_name.strip()
|
|
2043
|
+
if isinstance(explicit_name, str)
|
|
2044
|
+
else f"_temp_compactor_{uuid.uuid4().hex[:8]}"
|
|
2045
|
+
)
|
|
2046
|
+
|
|
2047
|
+
builder.register_compactor(temporary_name, config_dict)
|
|
2048
|
+
handle = CompactorHandle(temporary_name)
|
|
2049
|
+
_compactor_registry[temporary_name] = handle
|
|
2050
|
+
return handle
|
|
2051
|
+
|
|
1669
2052
|
def _process_agent_config(agent_name, config):
|
|
1670
2053
|
"""
|
|
1671
2054
|
Process agent configuration for both curried and direct syntax.
|
|
@@ -1718,6 +2101,11 @@ def create_dsl_stubs(
|
|
|
1718
2101
|
normalized.append(tool_entry)
|
|
1719
2102
|
config_dict["tools"] = normalized
|
|
1720
2103
|
|
|
2104
|
+
if "context" in config_dict:
|
|
2105
|
+
context_value = config_dict["context"]
|
|
2106
|
+
if hasattr(context_value, "name"):
|
|
2107
|
+
config_dict["context"] = context_value.name
|
|
2108
|
+
|
|
1721
2109
|
# Extract input schema if present
|
|
1722
2110
|
input_schema = None
|
|
1723
2111
|
if "input" in config_dict:
|
|
@@ -1903,6 +2291,11 @@ def create_dsl_stubs(
|
|
|
1903
2291
|
normalized.append(tool_entry)
|
|
1904
2292
|
config_dict["tools"] = normalized
|
|
1905
2293
|
|
|
2294
|
+
if "context" in config_dict:
|
|
2295
|
+
context_value = config_dict["context"]
|
|
2296
|
+
if hasattr(context_value, "name"):
|
|
2297
|
+
config_dict["context"] = context_value.name
|
|
2298
|
+
|
|
1906
2299
|
# Extract input schema if present
|
|
1907
2300
|
input_schema = None
|
|
1908
2301
|
if "input" in config_dict:
|
|
@@ -2001,13 +2394,21 @@ def create_dsl_stubs(
|
|
|
2001
2394
|
)
|
|
2002
2395
|
|
|
2003
2396
|
except Exception as error:
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2397
|
+
if isinstance(error, ValueError) and str(error).startswith("LM not configured"):
|
|
2398
|
+
# Two-phase initialization supports late LM setup (common in tests and
|
|
2399
|
+
# simple scripts). Treat this as an expected fallback rather than a hard error.
|
|
2400
|
+
logger.debug(
|
|
2401
|
+
"[AGENT_CREATION] Delayed creation for agent '%s': %s",
|
|
2402
|
+
temporary_agent_name,
|
|
2403
|
+
error,
|
|
2404
|
+
)
|
|
2405
|
+
else:
|
|
2406
|
+
logger.error(
|
|
2407
|
+
"[AGENT_CREATION] Failed to create agent '%s' immediately: %s",
|
|
2408
|
+
temporary_agent_name,
|
|
2409
|
+
error,
|
|
2410
|
+
exc_info=True,
|
|
2411
|
+
)
|
|
2011
2412
|
# Fall back to two-phase initialization if immediate creation fails
|
|
2012
2413
|
|
|
2013
2414
|
# Register handle for lookup
|
|
@@ -2086,7 +2487,14 @@ def create_dsl_stubs(
|
|
|
2086
2487
|
return classify_primitive(lua_table_to_dict(config))
|
|
2087
2488
|
|
|
2088
2489
|
binding_callback = _make_binding_callback(
|
|
2089
|
-
builder,
|
|
2490
|
+
builder,
|
|
2491
|
+
_tool_registry,
|
|
2492
|
+
_agent_registry,
|
|
2493
|
+
_context_registry,
|
|
2494
|
+
_corpus_registry,
|
|
2495
|
+
_retriever_registry,
|
|
2496
|
+
_compactor_registry,
|
|
2497
|
+
_runtime_context,
|
|
2090
2498
|
)
|
|
2091
2499
|
|
|
2092
2500
|
return {
|
|
@@ -2097,9 +2505,16 @@ def create_dsl_stubs(
|
|
|
2097
2505
|
"Agent": _new_agent, # NEW syntax - assignment based
|
|
2098
2506
|
"Model": HybridModel(_model, _Model),
|
|
2099
2507
|
"Procedure": _procedure,
|
|
2508
|
+
"Task": _task,
|
|
2509
|
+
"IncludeTasks": _include_tasks,
|
|
2100
2510
|
"Prompt": _prompt,
|
|
2101
2511
|
"Toolset": _toolset,
|
|
2102
2512
|
"Tool": _new_tool, # NEW syntax - assignment based
|
|
2513
|
+
"Context": _new_context,
|
|
2514
|
+
"Corpus": _new_corpus,
|
|
2515
|
+
"Retriever": _new_retriever,
|
|
2516
|
+
"Compactor": _new_compactor,
|
|
2517
|
+
"TaskFunction": _task_function,
|
|
2103
2518
|
"Classify": _new_classify, # NEW stdlib: smart classification with retry
|
|
2104
2519
|
"Hitl": _hitl,
|
|
2105
2520
|
"Specification": _specification,
|
|
@@ -2116,9 +2531,11 @@ def create_dsl_stubs(
|
|
|
2116
2531
|
"get_current_lm": _get_current_lm,
|
|
2117
2532
|
"Signature": _signature,
|
|
2118
2533
|
"Module": _module,
|
|
2119
|
-
"History":
|
|
2534
|
+
"History": _dspy_history,
|
|
2120
2535
|
"Message": _message,
|
|
2121
2536
|
"DSPyAgent": _dspy_agent,
|
|
2537
|
+
"_tactus_internal_corpus": _new_corpus,
|
|
2538
|
+
"_tactus_internal_retriever": _new_retriever,
|
|
2122
2539
|
# Script mode (top-level declarations)
|
|
2123
2540
|
"input": _input,
|
|
2124
2541
|
"output": _output,
|
|
@@ -2131,6 +2548,13 @@ def create_dsl_stubs(
|
|
|
2131
2548
|
"async": _async,
|
|
2132
2549
|
"max_depth": _max_depth,
|
|
2133
2550
|
"max_turns": _max_turns,
|
|
2551
|
+
# Context message directives
|
|
2552
|
+
"system": _system,
|
|
2553
|
+
"user": _user,
|
|
2554
|
+
"assistant": _assistant,
|
|
2555
|
+
"context": _context_insert,
|
|
2556
|
+
"history": _context_history,
|
|
2557
|
+
"template": _template,
|
|
2134
2558
|
# Built-in filters (exposed as a table)
|
|
2135
2559
|
"filters": {
|
|
2136
2560
|
"last_n": _last_n,
|
|
@@ -2155,6 +2579,10 @@ def create_dsl_stubs(
|
|
|
2155
2579
|
"agent": _agent_registry,
|
|
2156
2580
|
"tool": _tool_registry,
|
|
2157
2581
|
"model": _model_registry,
|
|
2582
|
+
"context": _context_registry,
|
|
2583
|
+
"corpus": _corpus_registry,
|
|
2584
|
+
"retriever": _retriever_registry,
|
|
2585
|
+
"compactor": _compactor_registry,
|
|
2158
2586
|
},
|
|
2159
2587
|
# Assignment interception callback
|
|
2160
2588
|
"_tactus_register_binding": binding_callback,
|
|
@@ -2162,7 +2590,14 @@ def create_dsl_stubs(
|
|
|
2162
2590
|
|
|
2163
2591
|
|
|
2164
2592
|
def _make_binding_callback(
|
|
2165
|
-
builder: RegistryBuilder,
|
|
2593
|
+
builder: RegistryBuilder,
|
|
2594
|
+
tool_registry: dict,
|
|
2595
|
+
agent_registry: dict,
|
|
2596
|
+
context_registry: dict,
|
|
2597
|
+
corpus_registry: dict,
|
|
2598
|
+
retriever_registry: dict,
|
|
2599
|
+
compactor_registry: dict,
|
|
2600
|
+
runtime_context: dict,
|
|
2166
2601
|
):
|
|
2167
2602
|
"""
|
|
2168
2603
|
Factory to create the binding callback with closure over builder/registries/runtime_context.
|
|
@@ -2170,6 +2605,12 @@ def _make_binding_callback(
|
|
|
2170
2605
|
This callback is called by Lua's __newindex metatable when assignments happen.
|
|
2171
2606
|
"""
|
|
2172
2607
|
import logging
|
|
2608
|
+
from tactus.primitives.handles import (
|
|
2609
|
+
CompactorHandle,
|
|
2610
|
+
ContextHandle,
|
|
2611
|
+
CorpusHandle,
|
|
2612
|
+
RetrieverHandle,
|
|
2613
|
+
)
|
|
2173
2614
|
from tactus.primitives.tool_handle import ToolHandle
|
|
2174
2615
|
|
|
2175
2616
|
callback_logger = logging.getLogger(__name__)
|
|
@@ -2254,6 +2695,105 @@ def _make_binding_callback(
|
|
|
2254
2695
|
f"[AGENT_RENAME] Updated _created_agents dict: '{old_name}' -> '{name}'"
|
|
2255
2696
|
)
|
|
2256
2697
|
|
|
2698
|
+
if isinstance(value, ContextHandle):
|
|
2699
|
+
old_name = value.name
|
|
2700
|
+
if old_name.startswith("_temp_context_"):
|
|
2701
|
+
callback_logger.debug(f"Renaming context '{old_name}' to '{name}'")
|
|
2702
|
+
value.name = name
|
|
2703
|
+
if old_name in context_registry:
|
|
2704
|
+
del context_registry[old_name]
|
|
2705
|
+
context_registry[name] = value
|
|
2706
|
+
if hasattr(builder, "registry") and old_name in builder.registry.contexts:
|
|
2707
|
+
context_data = builder.registry.contexts.pop(old_name)
|
|
2708
|
+
builder.registry.contexts[name] = context_data
|
|
2709
|
+
elif old_name != name:
|
|
2710
|
+
raise RuntimeError(
|
|
2711
|
+
f"Context name mismatch: assigned to '{name}' but context is named '{old_name}'. "
|
|
2712
|
+
"Remove the Context config 'name' field or make it match the assigned variable."
|
|
2713
|
+
)
|
|
2714
|
+
|
|
2715
|
+
if isinstance(value, CorpusHandle):
|
|
2716
|
+
old_name = value.name
|
|
2717
|
+
if old_name.startswith("_temp_corpus_"):
|
|
2718
|
+
callback_logger.debug(f"Renaming corpus '{old_name}' to '{name}'")
|
|
2719
|
+
value.name = name
|
|
2720
|
+
if old_name in corpus_registry:
|
|
2721
|
+
del corpus_registry[old_name]
|
|
2722
|
+
corpus_registry[name] = value
|
|
2723
|
+
if hasattr(builder, "registry") and old_name in builder.registry.corpora:
|
|
2724
|
+
corpus_data = builder.registry.corpora.pop(old_name)
|
|
2725
|
+
builder.registry.corpora[name] = corpus_data
|
|
2726
|
+
elif old_name != name:
|
|
2727
|
+
raise RuntimeError(
|
|
2728
|
+
f"Corpus name mismatch: assigned to '{name}' but corpus is named '{old_name}'. "
|
|
2729
|
+
"Remove the Corpus config 'name' field or make it match the assigned variable."
|
|
2730
|
+
)
|
|
2731
|
+
|
|
2732
|
+
if isinstance(value, RetrieverHandle):
|
|
2733
|
+
old_name = value.name
|
|
2734
|
+
if old_name.startswith("_temp_retriever_"):
|
|
2735
|
+
callback_logger.debug(f"Renaming retriever '{old_name}' to '{name}'")
|
|
2736
|
+
value.name = name
|
|
2737
|
+
if old_name in retriever_registry:
|
|
2738
|
+
del retriever_registry[old_name]
|
|
2739
|
+
retriever_registry[name] = value
|
|
2740
|
+
if hasattr(builder, "registry") and old_name in builder.registry.retrievers:
|
|
2741
|
+
retriever_data = builder.registry.retrievers.pop(old_name)
|
|
2742
|
+
builder.registry.retrievers[name] = retriever_data
|
|
2743
|
+
elif old_name != name:
|
|
2744
|
+
raise RuntimeError(
|
|
2745
|
+
f"Retriever name mismatch: assigned to '{name}' but retriever is named '{old_name}'. "
|
|
2746
|
+
"Remove the Retriever config 'name' field or make it match the assigned variable."
|
|
2747
|
+
)
|
|
2748
|
+
|
|
2749
|
+
if isinstance(value, CompactorHandle):
|
|
2750
|
+
old_name = value.name
|
|
2751
|
+
if old_name.startswith("_temp_compactor_"):
|
|
2752
|
+
callback_logger.debug(f"Renaming compactor '{old_name}' to '{name}'")
|
|
2753
|
+
value.name = name
|
|
2754
|
+
if old_name in compactor_registry:
|
|
2755
|
+
del compactor_registry[old_name]
|
|
2756
|
+
compactor_registry[name] = value
|
|
2757
|
+
if hasattr(builder, "registry") and old_name in builder.registry.compactors:
|
|
2758
|
+
compactor_data = builder.registry.compactors.pop(old_name)
|
|
2759
|
+
builder.registry.compactors[name] = compactor_data
|
|
2760
|
+
elif old_name != name:
|
|
2761
|
+
raise RuntimeError(
|
|
2762
|
+
f"Compactor name mismatch: assigned to '{name}' but compactor is named '{old_name}'. "
|
|
2763
|
+
"Remove the Compactor config 'name' field or make it match the assigned variable."
|
|
2764
|
+
)
|
|
2765
|
+
|
|
2766
|
+
if hasattr(value, "items"):
|
|
2767
|
+
try:
|
|
2768
|
+
task_config = lua_table_to_dict(value)
|
|
2769
|
+
except Exception:
|
|
2770
|
+
task_config = None
|
|
2771
|
+
if isinstance(task_config, dict) and task_config.get("__tactus_task_config"):
|
|
2772
|
+
if hasattr(builder, "registry") and name not in builder.registry.tasks:
|
|
2773
|
+
if "entry" in task_config and not callable(task_config["entry"]):
|
|
2774
|
+
raise TypeError(f"Task '{name}' entry must be a function")
|
|
2775
|
+
builder.register_task(name, task_config)
|
|
2776
|
+
|
|
2777
|
+
child_sources = task_config.get("__tactus_child_tasks")
|
|
2778
|
+
if isinstance(child_sources, dict):
|
|
2779
|
+
child_iter = child_sources.items()
|
|
2780
|
+
else:
|
|
2781
|
+
child_iter = value.items()
|
|
2782
|
+
|
|
2783
|
+
for key, child_value in child_iter:
|
|
2784
|
+
child_name = key if isinstance(key, str) else None
|
|
2785
|
+
if not hasattr(child_value, "items"):
|
|
2786
|
+
continue
|
|
2787
|
+
child_config = lua_table_to_dict(child_value)
|
|
2788
|
+
if not child_name and isinstance(child_config, dict):
|
|
2789
|
+
child_name = child_config.get("__task_name")
|
|
2790
|
+
if child_name and isinstance(child_config, dict):
|
|
2791
|
+
if "entry" in child_config and not callable(child_config["entry"]):
|
|
2792
|
+
raise TypeError(
|
|
2793
|
+
f"Task '{name}:{child_name}' entry must be a function"
|
|
2794
|
+
)
|
|
2795
|
+
builder.register_task(child_name, child_config, parent=name)
|
|
2796
|
+
|
|
2257
2797
|
# Log all assignments for debugging (only at trace level to avoid noise)
|
|
2258
2798
|
callback_logger.debug(f"Assignment captured: {name} = {type(value).__name__}")
|
|
2259
2799
|
|