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/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,100 @@ 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
|
+
if config is None:
|
|
417
|
+
|
|
418
|
+
def _curried(task_config=None):
|
|
419
|
+
if task_config is None or not hasattr(task_config, "items"):
|
|
420
|
+
raise TypeError(
|
|
421
|
+
f"Task '{task_name}' requires a configuration table. "
|
|
422
|
+
'Use: Task "name" { ... } or name = Task { ... }.'
|
|
423
|
+
)
|
|
424
|
+
return _task(task_name, task_config)
|
|
425
|
+
|
|
426
|
+
return _curried
|
|
427
|
+
task_config = config or {}
|
|
428
|
+
if hasattr(task_config, "__setitem__"):
|
|
429
|
+
try:
|
|
430
|
+
task_config["__task_name"] = task_name
|
|
431
|
+
task_config["__tactus_task_config"] = True
|
|
432
|
+
except Exception:
|
|
433
|
+
pass
|
|
434
|
+
task_config_dict = lua_table_to_dict(task_config)
|
|
435
|
+
if "entry" in task_config_dict and not callable(task_config_dict["entry"]):
|
|
436
|
+
raise TypeError(f"Task '{task_name}' entry must be a function")
|
|
437
|
+
builder.register_task(task_name, task_config_dict)
|
|
438
|
+
|
|
439
|
+
if hasattr(task_config, "items"):
|
|
440
|
+
for key, value in task_config.items():
|
|
441
|
+
child_name = None
|
|
442
|
+
if isinstance(key, str):
|
|
443
|
+
child_name = key
|
|
444
|
+
if not hasattr(value, "items"):
|
|
445
|
+
continue
|
|
446
|
+
child_config = lua_table_to_dict(value)
|
|
447
|
+
if not child_name and isinstance(child_config, dict):
|
|
448
|
+
child_name = child_config.get("__task_name")
|
|
449
|
+
if child_name and isinstance(child_config, dict) and "entry" in child_config:
|
|
450
|
+
builder.register_task(child_name, child_config, parent=task_name)
|
|
451
|
+
return task_config
|
|
452
|
+
|
|
453
|
+
# Assignment-based: name = Task { ... }
|
|
454
|
+
if name_or_config is not None and hasattr(name_or_config, "items"):
|
|
455
|
+
task_config = name_or_config
|
|
456
|
+
if hasattr(task_config, "__setitem__"):
|
|
457
|
+
try:
|
|
458
|
+
task_config["__tactus_task_config"] = True
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
if hasattr(task_config, "items"):
|
|
462
|
+
child_tasks = {}
|
|
463
|
+
for key, value in task_config.items():
|
|
464
|
+
if isinstance(key, str) and hasattr(value, "items"):
|
|
465
|
+
child_tasks[key] = value
|
|
466
|
+
if child_tasks:
|
|
467
|
+
try:
|
|
468
|
+
task_config["__tactus_child_tasks"] = child_tasks
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
return task_config
|
|
472
|
+
|
|
473
|
+
return {}
|
|
474
|
+
|
|
475
|
+
def _include_tasks(path=None, namespace=None):
|
|
476
|
+
"""IncludeTasks stub (records include for runtime)."""
|
|
477
|
+
if hasattr(path, "items"):
|
|
478
|
+
include_config = lua_table_to_dict(path)
|
|
479
|
+
include_path = include_config.get("path")
|
|
480
|
+
include_namespace = include_config.get("namespace")
|
|
481
|
+
if isinstance(include_path, str):
|
|
482
|
+
builder.register_include_tasks(include_path, include_namespace)
|
|
483
|
+
return None
|
|
484
|
+
if isinstance(path, str):
|
|
485
|
+
builder.register_include_tasks(path, namespace if isinstance(namespace, str) else None)
|
|
486
|
+
return None
|
|
362
487
|
|
|
363
488
|
def _prompt(prompt_name: str, content: str) -> None:
|
|
364
489
|
"""Register a prompt template."""
|
|
@@ -477,6 +602,67 @@ def create_dsl_stubs(
|
|
|
477
602
|
|
|
478
603
|
return accept_config
|
|
479
604
|
|
|
605
|
+
def _template(template_text: str, vars_table: Any = None) -> dict[str, Any]:
|
|
606
|
+
"""
|
|
607
|
+
Create a template directive for context messages.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
template_text: Template string with {input.*} or {context.*} markers
|
|
611
|
+
vars_table: Optional vars table for template values
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Dict describing the template directive
|
|
615
|
+
"""
|
|
616
|
+
if not isinstance(template_text, str):
|
|
617
|
+
raise TypeError("template() expects a string")
|
|
618
|
+
vars_dict = lua_table_to_dict(vars_table) if vars_table is not None else {}
|
|
619
|
+
vars_dict = _normalize_template_vars(vars_dict)
|
|
620
|
+
return {"template": template_text, "vars": vars_dict}
|
|
621
|
+
|
|
622
|
+
def _message_from_arg(message_type: str, arg: Any) -> dict[str, Any]:
|
|
623
|
+
"""Create a message directive from a literal or template directive."""
|
|
624
|
+
if isinstance(arg, dict) and "template" in arg:
|
|
625
|
+
return {
|
|
626
|
+
"type": message_type,
|
|
627
|
+
"template": arg.get("template"),
|
|
628
|
+
"vars": arg.get("vars", {}),
|
|
629
|
+
}
|
|
630
|
+
if isinstance(arg, str):
|
|
631
|
+
return {"type": message_type, "content": arg}
|
|
632
|
+
raise TypeError(f"{message_type}() expects a string or template(...) directive")
|
|
633
|
+
|
|
634
|
+
def _system(arg: Any) -> dict[str, Any]:
|
|
635
|
+
"""System message directive for Context messages."""
|
|
636
|
+
return _message_from_arg("system", arg)
|
|
637
|
+
|
|
638
|
+
def _user(arg: Any) -> dict[str, Any]:
|
|
639
|
+
"""User message directive for Context messages."""
|
|
640
|
+
return _message_from_arg("user", arg)
|
|
641
|
+
|
|
642
|
+
def _assistant(arg: Any) -> dict[str, Any]:
|
|
643
|
+
"""Assistant message directive for Context messages."""
|
|
644
|
+
return _message_from_arg("assistant", arg)
|
|
645
|
+
|
|
646
|
+
def _context_insert(pack: Any, budget: Any = None) -> dict[str, Any]:
|
|
647
|
+
"""Context pack insertion directive for Context messages."""
|
|
648
|
+
if hasattr(pack, "name"):
|
|
649
|
+
pack_name = pack.name
|
|
650
|
+
else:
|
|
651
|
+
pack_name = pack
|
|
652
|
+
if not isinstance(pack_name, str):
|
|
653
|
+
raise TypeError("context() expects a context or retriever reference")
|
|
654
|
+
budget_dict = lua_table_to_dict(budget) if budget is not None else None
|
|
655
|
+
if isinstance(budget_dict, list) and len(budget_dict) == 0:
|
|
656
|
+
budget_dict = None
|
|
657
|
+
directive = {"type": "context", "name": pack_name}
|
|
658
|
+
if budget_dict is not None:
|
|
659
|
+
directive["budget"] = budget_dict
|
|
660
|
+
return directive
|
|
661
|
+
|
|
662
|
+
def _context_history() -> dict[str, Any]:
|
|
663
|
+
"""History insertion directive for Context messages."""
|
|
664
|
+
return {"type": "history"}
|
|
665
|
+
|
|
480
666
|
def _specification(*args) -> None:
|
|
481
667
|
"""Register BDD specs.
|
|
482
668
|
|
|
@@ -1044,7 +1230,7 @@ def create_dsl_stubs(
|
|
|
1044
1230
|
# Otherwise, ignore unknown mock config.
|
|
1045
1231
|
continue
|
|
1046
1232
|
|
|
1047
|
-
def
|
|
1233
|
+
def _dspy_history(messages=None):
|
|
1048
1234
|
"""
|
|
1049
1235
|
Create a History for managing conversation messages.
|
|
1050
1236
|
|
|
@@ -1666,6 +1852,214 @@ def create_dsl_stubs(
|
|
|
1666
1852
|
|
|
1667
1853
|
return handle
|
|
1668
1854
|
|
|
1855
|
+
def _new_context(name_or_config=None) -> ContextHandle:
|
|
1856
|
+
"""
|
|
1857
|
+
New Context factory for assignment-based syntax.
|
|
1858
|
+
|
|
1859
|
+
Syntax:
|
|
1860
|
+
support_context = Context {
|
|
1861
|
+
policy = { ... },
|
|
1862
|
+
messages = { ... }
|
|
1863
|
+
}
|
|
1864
|
+
"""
|
|
1865
|
+
if isinstance(name_or_config, str):
|
|
1866
|
+
raise TypeError(
|
|
1867
|
+
"Curried Context syntax is not supported. Use assignment syntax: "
|
|
1868
|
+
"my_context = Context { ... }."
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
config = name_or_config
|
|
1872
|
+
if config is None:
|
|
1873
|
+
raise TypeError("Context requires a configuration table")
|
|
1874
|
+
|
|
1875
|
+
config_dict = lua_table_to_dict(config)
|
|
1876
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
1877
|
+
config_dict = {}
|
|
1878
|
+
|
|
1879
|
+
explicit_name = None
|
|
1880
|
+
if isinstance(config_dict, dict):
|
|
1881
|
+
explicit_name = config_dict.pop("name", None)
|
|
1882
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
1883
|
+
raise TypeError("Context 'name' must be a string")
|
|
1884
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
1885
|
+
raise TypeError("Context 'name' cannot be empty")
|
|
1886
|
+
|
|
1887
|
+
packs = config_dict.get("packs")
|
|
1888
|
+
|
|
1889
|
+
if isinstance(packs, list):
|
|
1890
|
+
config_dict["packs"] = [_normalize_context_pack_entry(pack) for pack in packs]
|
|
1891
|
+
elif packs is not None:
|
|
1892
|
+
config_dict["packs"] = [_normalize_context_pack_entry(packs)]
|
|
1893
|
+
|
|
1894
|
+
policy = config_dict.get("policy")
|
|
1895
|
+
if isinstance(policy, dict):
|
|
1896
|
+
compactor_value = policy.get("compactor")
|
|
1897
|
+
if hasattr(compactor_value, "name"):
|
|
1898
|
+
policy["compactor"] = compactor_value.name
|
|
1899
|
+
|
|
1900
|
+
import uuid
|
|
1901
|
+
|
|
1902
|
+
temporary_name = (
|
|
1903
|
+
explicit_name.strip()
|
|
1904
|
+
if isinstance(explicit_name, str)
|
|
1905
|
+
else f"_temp_context_{uuid.uuid4().hex[:8]}"
|
|
1906
|
+
)
|
|
1907
|
+
|
|
1908
|
+
builder.register_context(temporary_name, config_dict)
|
|
1909
|
+
handle = ContextHandle(temporary_name)
|
|
1910
|
+
_context_registry[temporary_name] = handle
|
|
1911
|
+
return handle
|
|
1912
|
+
|
|
1913
|
+
def _new_corpus(name_or_config=None) -> CorpusHandle:
|
|
1914
|
+
"""
|
|
1915
|
+
New Corpus factory for assignment-based syntax.
|
|
1916
|
+
"""
|
|
1917
|
+
if isinstance(name_or_config, str):
|
|
1918
|
+
raise TypeError(
|
|
1919
|
+
"Curried Corpus syntax is not supported. Use assignment syntax: "
|
|
1920
|
+
"my_corpus = Corpus { ... }."
|
|
1921
|
+
)
|
|
1922
|
+
|
|
1923
|
+
config = name_or_config
|
|
1924
|
+
if config is None:
|
|
1925
|
+
raise TypeError("Corpus requires a configuration table")
|
|
1926
|
+
|
|
1927
|
+
config_dict = lua_table_to_dict(config)
|
|
1928
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
1929
|
+
config_dict = {}
|
|
1930
|
+
|
|
1931
|
+
if isinstance(config_dict, dict):
|
|
1932
|
+
if "root" in config_dict and "corpus_root" not in config_dict:
|
|
1933
|
+
config_dict["corpus_root"] = config_dict.pop("root")
|
|
1934
|
+
|
|
1935
|
+
if isinstance(config_dict, dict):
|
|
1936
|
+
configuration = config_dict.get("configuration")
|
|
1937
|
+
if isinstance(configuration, dict):
|
|
1938
|
+
pipeline = configuration.get("pipeline")
|
|
1939
|
+
if isinstance(pipeline, list) and len(pipeline) == 0:
|
|
1940
|
+
configuration["pipeline"] = {}
|
|
1941
|
+
|
|
1942
|
+
explicit_name = None
|
|
1943
|
+
if isinstance(config_dict, dict):
|
|
1944
|
+
explicit_name = config_dict.pop("name", None)
|
|
1945
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
1946
|
+
raise TypeError("Corpus 'name' must be a string")
|
|
1947
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
1948
|
+
raise TypeError("Corpus 'name' cannot be empty")
|
|
1949
|
+
|
|
1950
|
+
import uuid
|
|
1951
|
+
|
|
1952
|
+
temporary_name = (
|
|
1953
|
+
explicit_name.strip()
|
|
1954
|
+
if isinstance(explicit_name, str)
|
|
1955
|
+
else f"_temp_corpus_{uuid.uuid4().hex[:8]}"
|
|
1956
|
+
)
|
|
1957
|
+
|
|
1958
|
+
builder.register_corpus(temporary_name, config_dict)
|
|
1959
|
+
handle = CorpusHandle(temporary_name)
|
|
1960
|
+
_corpus_registry[temporary_name] = handle
|
|
1961
|
+
return handle
|
|
1962
|
+
|
|
1963
|
+
def _new_retriever(name_or_config=None) -> RetrieverHandle:
|
|
1964
|
+
"""
|
|
1965
|
+
New Retriever factory for assignment-based syntax.
|
|
1966
|
+
"""
|
|
1967
|
+
if isinstance(name_or_config, str):
|
|
1968
|
+
raise TypeError(
|
|
1969
|
+
"Curried Retriever syntax is not supported. Use assignment syntax: "
|
|
1970
|
+
"my_retriever = Retriever { ... }."
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
config = name_or_config
|
|
1974
|
+
if config is None:
|
|
1975
|
+
raise TypeError("Retriever requires a configuration table")
|
|
1976
|
+
|
|
1977
|
+
config_dict = lua_table_to_dict(config)
|
|
1978
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
1979
|
+
config_dict = {}
|
|
1980
|
+
|
|
1981
|
+
if isinstance(config_dict, dict):
|
|
1982
|
+
config_dict["corpus"] = _normalize_handle_name(config_dict.get("corpus"))
|
|
1983
|
+
|
|
1984
|
+
explicit_name = None
|
|
1985
|
+
if isinstance(config_dict, dict):
|
|
1986
|
+
explicit_name = config_dict.pop("name", None)
|
|
1987
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
1988
|
+
raise TypeError("Retriever 'name' must be a string")
|
|
1989
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
1990
|
+
raise TypeError("Retriever 'name' cannot be empty")
|
|
1991
|
+
|
|
1992
|
+
import uuid
|
|
1993
|
+
|
|
1994
|
+
temporary_name = (
|
|
1995
|
+
explicit_name.strip()
|
|
1996
|
+
if isinstance(explicit_name, str)
|
|
1997
|
+
else f"_temp_retriever_{uuid.uuid4().hex[:8]}"
|
|
1998
|
+
)
|
|
1999
|
+
|
|
2000
|
+
builder.register_retriever(temporary_name, config_dict)
|
|
2001
|
+
handle = RetrieverHandle(temporary_name)
|
|
2002
|
+
_retriever_registry[temporary_name] = handle
|
|
2003
|
+
return handle
|
|
2004
|
+
|
|
2005
|
+
def _task_function(function):
|
|
2006
|
+
"""
|
|
2007
|
+
Wrap a callable so Task entries can use call-style syntax without executing immediately.
|
|
2008
|
+
"""
|
|
2009
|
+
if not callable(function):
|
|
2010
|
+
raise TypeError("TaskFunction expects a callable")
|
|
2011
|
+
|
|
2012
|
+
def _deferred(args):
|
|
2013
|
+
def _runner():
|
|
2014
|
+
if hasattr(args, "items"):
|
|
2015
|
+
args_dict = lua_table_to_dict(args)
|
|
2016
|
+
else:
|
|
2017
|
+
args_dict = args
|
|
2018
|
+
return function(args_dict)
|
|
2019
|
+
|
|
2020
|
+
return _runner
|
|
2021
|
+
|
|
2022
|
+
return _deferred
|
|
2023
|
+
|
|
2024
|
+
def _new_compactor(name_or_config=None) -> CompactorHandle:
|
|
2025
|
+
"""
|
|
2026
|
+
New Compactor factory for assignment-based syntax.
|
|
2027
|
+
"""
|
|
2028
|
+
if isinstance(name_or_config, str):
|
|
2029
|
+
raise TypeError(
|
|
2030
|
+
"Curried Compactor syntax is not supported. Use assignment syntax: "
|
|
2031
|
+
"my_compactor = Compactor { ... }."
|
|
2032
|
+
)
|
|
2033
|
+
|
|
2034
|
+
config = name_or_config
|
|
2035
|
+
if config is None:
|
|
2036
|
+
raise TypeError("Compactor requires a configuration table")
|
|
2037
|
+
|
|
2038
|
+
config_dict = lua_table_to_dict(config)
|
|
2039
|
+
if isinstance(config_dict, list) and len(config_dict) == 0:
|
|
2040
|
+
config_dict = {}
|
|
2041
|
+
|
|
2042
|
+
explicit_name = None
|
|
2043
|
+
if isinstance(config_dict, dict):
|
|
2044
|
+
explicit_name = config_dict.pop("name", None)
|
|
2045
|
+
if explicit_name is not None and not isinstance(explicit_name, str):
|
|
2046
|
+
raise TypeError("Compactor 'name' must be a string")
|
|
2047
|
+
if isinstance(explicit_name, str) and not explicit_name.strip():
|
|
2048
|
+
raise TypeError("Compactor 'name' cannot be empty")
|
|
2049
|
+
|
|
2050
|
+
import uuid
|
|
2051
|
+
|
|
2052
|
+
temporary_name = (
|
|
2053
|
+
explicit_name.strip()
|
|
2054
|
+
if isinstance(explicit_name, str)
|
|
2055
|
+
else f"_temp_compactor_{uuid.uuid4().hex[:8]}"
|
|
2056
|
+
)
|
|
2057
|
+
|
|
2058
|
+
builder.register_compactor(temporary_name, config_dict)
|
|
2059
|
+
handle = CompactorHandle(temporary_name)
|
|
2060
|
+
_compactor_registry[temporary_name] = handle
|
|
2061
|
+
return handle
|
|
2062
|
+
|
|
1669
2063
|
def _process_agent_config(agent_name, config):
|
|
1670
2064
|
"""
|
|
1671
2065
|
Process agent configuration for both curried and direct syntax.
|
|
@@ -1718,6 +2112,11 @@ def create_dsl_stubs(
|
|
|
1718
2112
|
normalized.append(tool_entry)
|
|
1719
2113
|
config_dict["tools"] = normalized
|
|
1720
2114
|
|
|
2115
|
+
if "context" in config_dict:
|
|
2116
|
+
context_value = config_dict["context"]
|
|
2117
|
+
if hasattr(context_value, "name"):
|
|
2118
|
+
config_dict["context"] = context_value.name
|
|
2119
|
+
|
|
1721
2120
|
# Extract input schema if present
|
|
1722
2121
|
input_schema = None
|
|
1723
2122
|
if "input" in config_dict:
|
|
@@ -1903,6 +2302,11 @@ def create_dsl_stubs(
|
|
|
1903
2302
|
normalized.append(tool_entry)
|
|
1904
2303
|
config_dict["tools"] = normalized
|
|
1905
2304
|
|
|
2305
|
+
if "context" in config_dict:
|
|
2306
|
+
context_value = config_dict["context"]
|
|
2307
|
+
if hasattr(context_value, "name"):
|
|
2308
|
+
config_dict["context"] = context_value.name
|
|
2309
|
+
|
|
1906
2310
|
# Extract input schema if present
|
|
1907
2311
|
input_schema = None
|
|
1908
2312
|
if "input" in config_dict:
|
|
@@ -2001,13 +2405,21 @@ def create_dsl_stubs(
|
|
|
2001
2405
|
)
|
|
2002
2406
|
|
|
2003
2407
|
except Exception as error:
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2408
|
+
if isinstance(error, ValueError) and str(error).startswith("LM not configured"):
|
|
2409
|
+
# Two-phase initialization supports late LM setup (common in tests and
|
|
2410
|
+
# simple scripts). Treat this as an expected fallback rather than a hard error.
|
|
2411
|
+
logger.debug(
|
|
2412
|
+
"[AGENT_CREATION] Delayed creation for agent '%s': %s",
|
|
2413
|
+
temporary_agent_name,
|
|
2414
|
+
error,
|
|
2415
|
+
)
|
|
2416
|
+
else:
|
|
2417
|
+
logger.error(
|
|
2418
|
+
"[AGENT_CREATION] Failed to create agent '%s' immediately: %s",
|
|
2419
|
+
temporary_agent_name,
|
|
2420
|
+
error,
|
|
2421
|
+
exc_info=True,
|
|
2422
|
+
)
|
|
2011
2423
|
# Fall back to two-phase initialization if immediate creation fails
|
|
2012
2424
|
|
|
2013
2425
|
# Register handle for lookup
|
|
@@ -2086,7 +2498,14 @@ def create_dsl_stubs(
|
|
|
2086
2498
|
return classify_primitive(lua_table_to_dict(config))
|
|
2087
2499
|
|
|
2088
2500
|
binding_callback = _make_binding_callback(
|
|
2089
|
-
builder,
|
|
2501
|
+
builder,
|
|
2502
|
+
_tool_registry,
|
|
2503
|
+
_agent_registry,
|
|
2504
|
+
_context_registry,
|
|
2505
|
+
_corpus_registry,
|
|
2506
|
+
_retriever_registry,
|
|
2507
|
+
_compactor_registry,
|
|
2508
|
+
_runtime_context,
|
|
2090
2509
|
)
|
|
2091
2510
|
|
|
2092
2511
|
return {
|
|
@@ -2097,9 +2516,16 @@ def create_dsl_stubs(
|
|
|
2097
2516
|
"Agent": _new_agent, # NEW syntax - assignment based
|
|
2098
2517
|
"Model": HybridModel(_model, _Model),
|
|
2099
2518
|
"Procedure": _procedure,
|
|
2519
|
+
"Task": _task,
|
|
2520
|
+
"IncludeTasks": _include_tasks,
|
|
2100
2521
|
"Prompt": _prompt,
|
|
2101
2522
|
"Toolset": _toolset,
|
|
2102
2523
|
"Tool": _new_tool, # NEW syntax - assignment based
|
|
2524
|
+
"Context": _new_context,
|
|
2525
|
+
"Corpus": _new_corpus,
|
|
2526
|
+
"Retriever": _new_retriever,
|
|
2527
|
+
"Compactor": _new_compactor,
|
|
2528
|
+
"TaskFunction": _task_function,
|
|
2103
2529
|
"Classify": _new_classify, # NEW stdlib: smart classification with retry
|
|
2104
2530
|
"Hitl": _hitl,
|
|
2105
2531
|
"Specification": _specification,
|
|
@@ -2116,9 +2542,11 @@ def create_dsl_stubs(
|
|
|
2116
2542
|
"get_current_lm": _get_current_lm,
|
|
2117
2543
|
"Signature": _signature,
|
|
2118
2544
|
"Module": _module,
|
|
2119
|
-
"History":
|
|
2545
|
+
"History": _dspy_history,
|
|
2120
2546
|
"Message": _message,
|
|
2121
2547
|
"DSPyAgent": _dspy_agent,
|
|
2548
|
+
"_tactus_internal_corpus": _new_corpus,
|
|
2549
|
+
"_tactus_internal_retriever": _new_retriever,
|
|
2122
2550
|
# Script mode (top-level declarations)
|
|
2123
2551
|
"input": _input,
|
|
2124
2552
|
"output": _output,
|
|
@@ -2131,6 +2559,13 @@ def create_dsl_stubs(
|
|
|
2131
2559
|
"async": _async,
|
|
2132
2560
|
"max_depth": _max_depth,
|
|
2133
2561
|
"max_turns": _max_turns,
|
|
2562
|
+
# Context message directives
|
|
2563
|
+
"system": _system,
|
|
2564
|
+
"user": _user,
|
|
2565
|
+
"assistant": _assistant,
|
|
2566
|
+
"context": _context_insert,
|
|
2567
|
+
"history": _context_history,
|
|
2568
|
+
"template": _template,
|
|
2134
2569
|
# Built-in filters (exposed as a table)
|
|
2135
2570
|
"filters": {
|
|
2136
2571
|
"last_n": _last_n,
|
|
@@ -2155,6 +2590,10 @@ def create_dsl_stubs(
|
|
|
2155
2590
|
"agent": _agent_registry,
|
|
2156
2591
|
"tool": _tool_registry,
|
|
2157
2592
|
"model": _model_registry,
|
|
2593
|
+
"context": _context_registry,
|
|
2594
|
+
"corpus": _corpus_registry,
|
|
2595
|
+
"retriever": _retriever_registry,
|
|
2596
|
+
"compactor": _compactor_registry,
|
|
2158
2597
|
},
|
|
2159
2598
|
# Assignment interception callback
|
|
2160
2599
|
"_tactus_register_binding": binding_callback,
|
|
@@ -2162,7 +2601,14 @@ def create_dsl_stubs(
|
|
|
2162
2601
|
|
|
2163
2602
|
|
|
2164
2603
|
def _make_binding_callback(
|
|
2165
|
-
builder: RegistryBuilder,
|
|
2604
|
+
builder: RegistryBuilder,
|
|
2605
|
+
tool_registry: dict,
|
|
2606
|
+
agent_registry: dict,
|
|
2607
|
+
context_registry: dict,
|
|
2608
|
+
corpus_registry: dict,
|
|
2609
|
+
retriever_registry: dict,
|
|
2610
|
+
compactor_registry: dict,
|
|
2611
|
+
runtime_context: dict,
|
|
2166
2612
|
):
|
|
2167
2613
|
"""
|
|
2168
2614
|
Factory to create the binding callback with closure over builder/registries/runtime_context.
|
|
@@ -2170,6 +2616,12 @@ def _make_binding_callback(
|
|
|
2170
2616
|
This callback is called by Lua's __newindex metatable when assignments happen.
|
|
2171
2617
|
"""
|
|
2172
2618
|
import logging
|
|
2619
|
+
from tactus.primitives.handles import (
|
|
2620
|
+
CompactorHandle,
|
|
2621
|
+
ContextHandle,
|
|
2622
|
+
CorpusHandle,
|
|
2623
|
+
RetrieverHandle,
|
|
2624
|
+
)
|
|
2173
2625
|
from tactus.primitives.tool_handle import ToolHandle
|
|
2174
2626
|
|
|
2175
2627
|
callback_logger = logging.getLogger(__name__)
|
|
@@ -2254,6 +2706,105 @@ def _make_binding_callback(
|
|
|
2254
2706
|
f"[AGENT_RENAME] Updated _created_agents dict: '{old_name}' -> '{name}'"
|
|
2255
2707
|
)
|
|
2256
2708
|
|
|
2709
|
+
if isinstance(value, ContextHandle):
|
|
2710
|
+
old_name = value.name
|
|
2711
|
+
if old_name.startswith("_temp_context_"):
|
|
2712
|
+
callback_logger.debug(f"Renaming context '{old_name}' to '{name}'")
|
|
2713
|
+
value.name = name
|
|
2714
|
+
if old_name in context_registry:
|
|
2715
|
+
del context_registry[old_name]
|
|
2716
|
+
context_registry[name] = value
|
|
2717
|
+
if hasattr(builder, "registry") and old_name in builder.registry.contexts:
|
|
2718
|
+
context_data = builder.registry.contexts.pop(old_name)
|
|
2719
|
+
builder.registry.contexts[name] = context_data
|
|
2720
|
+
elif old_name != name:
|
|
2721
|
+
raise RuntimeError(
|
|
2722
|
+
f"Context name mismatch: assigned to '{name}' but context is named '{old_name}'. "
|
|
2723
|
+
"Remove the Context config 'name' field or make it match the assigned variable."
|
|
2724
|
+
)
|
|
2725
|
+
|
|
2726
|
+
if isinstance(value, CorpusHandle):
|
|
2727
|
+
old_name = value.name
|
|
2728
|
+
if old_name.startswith("_temp_corpus_"):
|
|
2729
|
+
callback_logger.debug(f"Renaming corpus '{old_name}' to '{name}'")
|
|
2730
|
+
value.name = name
|
|
2731
|
+
if old_name in corpus_registry:
|
|
2732
|
+
del corpus_registry[old_name]
|
|
2733
|
+
corpus_registry[name] = value
|
|
2734
|
+
if hasattr(builder, "registry") and old_name in builder.registry.corpora:
|
|
2735
|
+
corpus_data = builder.registry.corpora.pop(old_name)
|
|
2736
|
+
builder.registry.corpora[name] = corpus_data
|
|
2737
|
+
elif old_name != name:
|
|
2738
|
+
raise RuntimeError(
|
|
2739
|
+
f"Corpus name mismatch: assigned to '{name}' but corpus is named '{old_name}'. "
|
|
2740
|
+
"Remove the Corpus config 'name' field or make it match the assigned variable."
|
|
2741
|
+
)
|
|
2742
|
+
|
|
2743
|
+
if isinstance(value, RetrieverHandle):
|
|
2744
|
+
old_name = value.name
|
|
2745
|
+
if old_name.startswith("_temp_retriever_"):
|
|
2746
|
+
callback_logger.debug(f"Renaming retriever '{old_name}' to '{name}'")
|
|
2747
|
+
value.name = name
|
|
2748
|
+
if old_name in retriever_registry:
|
|
2749
|
+
del retriever_registry[old_name]
|
|
2750
|
+
retriever_registry[name] = value
|
|
2751
|
+
if hasattr(builder, "registry") and old_name in builder.registry.retrievers:
|
|
2752
|
+
retriever_data = builder.registry.retrievers.pop(old_name)
|
|
2753
|
+
builder.registry.retrievers[name] = retriever_data
|
|
2754
|
+
elif old_name != name:
|
|
2755
|
+
raise RuntimeError(
|
|
2756
|
+
f"Retriever name mismatch: assigned to '{name}' but retriever is named '{old_name}'. "
|
|
2757
|
+
"Remove the Retriever config 'name' field or make it match the assigned variable."
|
|
2758
|
+
)
|
|
2759
|
+
|
|
2760
|
+
if isinstance(value, CompactorHandle):
|
|
2761
|
+
old_name = value.name
|
|
2762
|
+
if old_name.startswith("_temp_compactor_"):
|
|
2763
|
+
callback_logger.debug(f"Renaming compactor '{old_name}' to '{name}'")
|
|
2764
|
+
value.name = name
|
|
2765
|
+
if old_name in compactor_registry:
|
|
2766
|
+
del compactor_registry[old_name]
|
|
2767
|
+
compactor_registry[name] = value
|
|
2768
|
+
if hasattr(builder, "registry") and old_name in builder.registry.compactors:
|
|
2769
|
+
compactor_data = builder.registry.compactors.pop(old_name)
|
|
2770
|
+
builder.registry.compactors[name] = compactor_data
|
|
2771
|
+
elif old_name != name:
|
|
2772
|
+
raise RuntimeError(
|
|
2773
|
+
f"Compactor name mismatch: assigned to '{name}' but compactor is named '{old_name}'. "
|
|
2774
|
+
"Remove the Compactor config 'name' field or make it match the assigned variable."
|
|
2775
|
+
)
|
|
2776
|
+
|
|
2777
|
+
if hasattr(value, "items"):
|
|
2778
|
+
try:
|
|
2779
|
+
task_config = lua_table_to_dict(value)
|
|
2780
|
+
except Exception:
|
|
2781
|
+
task_config = None
|
|
2782
|
+
if isinstance(task_config, dict) and task_config.get("__tactus_task_config"):
|
|
2783
|
+
if hasattr(builder, "registry") and name not in builder.registry.tasks:
|
|
2784
|
+
if "entry" in task_config and not callable(task_config["entry"]):
|
|
2785
|
+
raise TypeError(f"Task '{name}' entry must be a function")
|
|
2786
|
+
builder.register_task(name, task_config)
|
|
2787
|
+
|
|
2788
|
+
child_sources = task_config.get("__tactus_child_tasks")
|
|
2789
|
+
if isinstance(child_sources, dict):
|
|
2790
|
+
child_iter = child_sources.items()
|
|
2791
|
+
else:
|
|
2792
|
+
child_iter = value.items()
|
|
2793
|
+
|
|
2794
|
+
for key, child_value in child_iter:
|
|
2795
|
+
child_name = key if isinstance(key, str) else None
|
|
2796
|
+
if not hasattr(child_value, "items"):
|
|
2797
|
+
continue
|
|
2798
|
+
child_config = lua_table_to_dict(child_value)
|
|
2799
|
+
if not child_name and isinstance(child_config, dict):
|
|
2800
|
+
child_name = child_config.get("__task_name")
|
|
2801
|
+
if child_name and isinstance(child_config, dict):
|
|
2802
|
+
if "entry" in child_config and not callable(child_config["entry"]):
|
|
2803
|
+
raise TypeError(
|
|
2804
|
+
f"Task '{name}:{child_name}' entry must be a function"
|
|
2805
|
+
)
|
|
2806
|
+
builder.register_task(child_name, child_config, parent=name)
|
|
2807
|
+
|
|
2257
2808
|
# Log all assignments for debugging (only at trace level to avoid noise)
|
|
2258
2809
|
callback_logger.debug(f"Assignment captured: {name} = {type(value).__name__}")
|
|
2259
2810
|
|