tactus 0.31.2__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 +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
tactus/stdlib/io/hdf5.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tactus.io.hdf5 - HDF5 file operations for Tactus.
|
|
3
|
+
|
|
4
|
+
Provides HDF5 dataset read/write operations with sandboxing
|
|
5
|
+
to the procedure's base directory.
|
|
6
|
+
|
|
7
|
+
Requires: h5py
|
|
8
|
+
|
|
9
|
+
Usage in .tac files:
|
|
10
|
+
local hdf5 = require("tactus.io.hdf5")
|
|
11
|
+
|
|
12
|
+
-- Read dataset from HDF5 file
|
|
13
|
+
local data = hdf5.read("data.h5", "measurements/temperature")
|
|
14
|
+
|
|
15
|
+
-- Write dataset to HDF5 file
|
|
16
|
+
hdf5.write("output.h5", "results/scores", {95, 87, 92, 88})
|
|
17
|
+
|
|
18
|
+
-- List all datasets in file
|
|
19
|
+
local datasets = hdf5.list("data.h5")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any, List
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import h5py
|
|
28
|
+
import numpy as np
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise ImportError("h5py is required for HDF5 support. Install with: pip install h5py")
|
|
31
|
+
|
|
32
|
+
# Get context (injected by loader)
|
|
33
|
+
_ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read(filepath: str, dataset: str) -> List[Any]:
|
|
37
|
+
"""
|
|
38
|
+
Read dataset from HDF5 file.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
filepath: Path to HDF5 file (relative to working directory)
|
|
42
|
+
dataset: Dataset path within the HDF5 file (e.g., "group/dataset")
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dataset contents as a list
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
FileNotFoundError: If file does not exist
|
|
49
|
+
KeyError: If dataset does not exist in file
|
|
50
|
+
PermissionError: If path is outside working directory
|
|
51
|
+
"""
|
|
52
|
+
if _ctx:
|
|
53
|
+
filepath = _ctx.validate_path(filepath)
|
|
54
|
+
|
|
55
|
+
with h5py.File(filepath, "r") as f:
|
|
56
|
+
return f[dataset][:].tolist()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write(filepath: str, dataset: str, data: List[Any]) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Write data to HDF5 dataset.
|
|
62
|
+
|
|
63
|
+
If the dataset already exists, it will be replaced.
|
|
64
|
+
Parent groups will be created automatically.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
filepath: Path to HDF5 file
|
|
68
|
+
dataset: Dataset path within the HDF5 file (e.g., "group/dataset")
|
|
69
|
+
data: Data to write (list or nested lists for multi-dimensional arrays)
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
PermissionError: If path is outside working directory
|
|
73
|
+
ValueError: If data is invalid
|
|
74
|
+
"""
|
|
75
|
+
if _ctx:
|
|
76
|
+
filepath = _ctx.validate_path(filepath)
|
|
77
|
+
|
|
78
|
+
# Create parent directories
|
|
79
|
+
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
|
80
|
+
|
|
81
|
+
with h5py.File(filepath, "a") as f:
|
|
82
|
+
# Delete existing dataset if present
|
|
83
|
+
if dataset in f:
|
|
84
|
+
del f[dataset]
|
|
85
|
+
|
|
86
|
+
# Create dataset
|
|
87
|
+
f.create_dataset(dataset, data=np.array(data))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list(filepath: str) -> List[str]:
|
|
91
|
+
"""
|
|
92
|
+
List all datasets in HDF5 file.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
filepath: Path to HDF5 file
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of dataset paths
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
FileNotFoundError: If file does not exist
|
|
102
|
+
PermissionError: If path is outside working directory
|
|
103
|
+
"""
|
|
104
|
+
if _ctx:
|
|
105
|
+
filepath = _ctx.validate_path(filepath)
|
|
106
|
+
|
|
107
|
+
datasets = []
|
|
108
|
+
|
|
109
|
+
with h5py.File(filepath, "r") as f:
|
|
110
|
+
|
|
111
|
+
def visitor(name, obj):
|
|
112
|
+
if isinstance(obj, h5py.Dataset):
|
|
113
|
+
datasets.append(name)
|
|
114
|
+
|
|
115
|
+
f.visititems(visitor)
|
|
116
|
+
|
|
117
|
+
return datasets
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Explicit exports
|
|
121
|
+
__tactus_exports__ = ["read", "write", "list"]
|
tactus/stdlib/io/json.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tactus.io.json - JSON file operations for Tactus.
|
|
3
|
+
|
|
4
|
+
Provides JSON read/write operations with automatic path validation
|
|
5
|
+
and sandboxing to the procedure's base directory.
|
|
6
|
+
|
|
7
|
+
Usage in .tac files:
|
|
8
|
+
local json = require("tactus.io.json")
|
|
9
|
+
|
|
10
|
+
-- Read JSON file
|
|
11
|
+
local data = json.read("config.json")
|
|
12
|
+
|
|
13
|
+
-- Write JSON file
|
|
14
|
+
json.write("output.json", {name = "Alice", score = 95})
|
|
15
|
+
|
|
16
|
+
-- Encode/decode strings
|
|
17
|
+
local str = json.encode({key = "value"})
|
|
18
|
+
local obj = json.decode('{"key": "value"}')
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
# Get context (injected by loader)
|
|
27
|
+
_ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read(filepath: str) -> Any:
|
|
31
|
+
"""
|
|
32
|
+
Read and parse a JSON file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
filepath: Path to JSON file (relative to working directory)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Parsed JSON data (dict, list, or primitive)
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileNotFoundError: If file does not exist
|
|
42
|
+
json.JSONDecodeError: If file is not valid JSON
|
|
43
|
+
PermissionError: If path is outside working directory
|
|
44
|
+
"""
|
|
45
|
+
if _ctx:
|
|
46
|
+
filepath = _ctx.validate_path(filepath)
|
|
47
|
+
|
|
48
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
49
|
+
return json.load(f)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def write(filepath: str, data: Any, indent: int = 2) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Write data to a JSON file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
filepath: Path to JSON file
|
|
58
|
+
data: Data to write (dict, list, or primitive)
|
|
59
|
+
indent: Indentation level (default 2)
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
PermissionError: If path is outside working directory
|
|
63
|
+
TypeError: If data is not JSON serializable
|
|
64
|
+
"""
|
|
65
|
+
if _ctx:
|
|
66
|
+
filepath = _ctx.validate_path(filepath)
|
|
67
|
+
|
|
68
|
+
# Create parent directories
|
|
69
|
+
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
|
70
|
+
|
|
71
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
72
|
+
json.dump(data, f, indent=indent, ensure_ascii=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def encode(data: Any, indent: Optional[int] = None) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Encode data to JSON string.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data: Data to encode
|
|
81
|
+
indent: Optional indentation
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
JSON string
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
TypeError: If data is not JSON serializable
|
|
88
|
+
"""
|
|
89
|
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def decode(json_string: str) -> Any:
|
|
93
|
+
"""
|
|
94
|
+
Decode JSON string to data.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
json_string: JSON string to parse
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Parsed data
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
json.JSONDecodeError: If string is not valid JSON
|
|
104
|
+
"""
|
|
105
|
+
return json.loads(json_string)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Explicit exports (only these functions exposed to Lua)
|
|
109
|
+
__tactus_exports__ = ["read", "write", "encode", "decode"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tactus.io.parquet - Apache Parquet file operations for Tactus.
|
|
3
|
+
|
|
4
|
+
Provides Parquet read/write operations with sandboxing
|
|
5
|
+
to the procedure's base directory.
|
|
6
|
+
|
|
7
|
+
Requires: pyarrow
|
|
8
|
+
|
|
9
|
+
Usage in .tac files:
|
|
10
|
+
local parquet = require("tactus.io.parquet")
|
|
11
|
+
|
|
12
|
+
-- Read Parquet file
|
|
13
|
+
local data = parquet.read("data.parquet")
|
|
14
|
+
|
|
15
|
+
-- Write Parquet file
|
|
16
|
+
parquet.write("output.parquet", {
|
|
17
|
+
{name = "Alice", score = 95},
|
|
18
|
+
{name = "Bob", score = 87}
|
|
19
|
+
})
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any, Dict, List
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import pyarrow as pa
|
|
28
|
+
import pyarrow.parquet as pq
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise ImportError("pyarrow is required for Parquet support. Install with: pip install pyarrow")
|
|
31
|
+
|
|
32
|
+
# Get context (injected by loader)
|
|
33
|
+
_ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read(filepath: str) -> List[Dict[str, Any]]:
|
|
37
|
+
"""
|
|
38
|
+
Read Parquet file, returning list of dictionaries.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
filepath: Path to Parquet file (relative to working directory)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of dictionaries, one per row
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
FileNotFoundError: If file does not exist
|
|
48
|
+
PermissionError: If path is outside working directory
|
|
49
|
+
"""
|
|
50
|
+
if _ctx:
|
|
51
|
+
filepath = _ctx.validate_path(filepath)
|
|
52
|
+
|
|
53
|
+
table = pq.read_table(filepath)
|
|
54
|
+
return table.to_pylist()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write(filepath: str, data: List[Dict[str, Any]]) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Write list of dictionaries to Parquet file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
filepath: Path to Parquet file
|
|
63
|
+
data: List of dictionaries to write
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
PermissionError: If path is outside working directory
|
|
67
|
+
ValueError: If data is empty or invalid
|
|
68
|
+
"""
|
|
69
|
+
if _ctx:
|
|
70
|
+
filepath = _ctx.validate_path(filepath)
|
|
71
|
+
|
|
72
|
+
if not data:
|
|
73
|
+
raise ValueError("Cannot write empty data to Parquet")
|
|
74
|
+
|
|
75
|
+
# Create parent directories
|
|
76
|
+
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
|
77
|
+
|
|
78
|
+
table = pa.Table.from_pylist(data)
|
|
79
|
+
pq.write_table(table, filepath)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Explicit exports
|
|
83
|
+
__tactus_exports__ = ["read", "write"]
|
tactus/stdlib/io/tsv.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tactus.io.tsv - TSV (tab-separated values) file operations for Tactus.
|
|
3
|
+
|
|
4
|
+
Provides TSV read/write operations with automatic header handling
|
|
5
|
+
and sandboxing to the procedure's base directory.
|
|
6
|
+
|
|
7
|
+
Usage in .tac files:
|
|
8
|
+
local tsv = require("tactus.io.tsv")
|
|
9
|
+
|
|
10
|
+
-- Read TSV file
|
|
11
|
+
local data = tsv.read("data.tsv")
|
|
12
|
+
|
|
13
|
+
-- Write TSV file
|
|
14
|
+
tsv.write("output.tsv", {
|
|
15
|
+
{name = "Alice", age = 30},
|
|
16
|
+
{name = "Bob", age = 25}
|
|
17
|
+
})
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import csv
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
# Get context (injected by loader)
|
|
26
|
+
_ctx = getattr(sys.modules[__name__], "__tactus_context__", None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read(filepath: str) -> List[Dict[str, Any]]:
|
|
30
|
+
"""
|
|
31
|
+
Read TSV file, returning list of dictionaries with headers as keys.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
filepath: Path to TSV file (relative to working directory)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of dictionaries, one per row
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
FileNotFoundError: If file does not exist
|
|
41
|
+
PermissionError: If path is outside working directory
|
|
42
|
+
"""
|
|
43
|
+
if _ctx:
|
|
44
|
+
filepath = _ctx.validate_path(filepath)
|
|
45
|
+
|
|
46
|
+
with open(filepath, "r", encoding="utf-8", newline="") as f:
|
|
47
|
+
reader = csv.DictReader(f, delimiter="\t")
|
|
48
|
+
return list(reader)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def write(
|
|
52
|
+
filepath: str, data: List[Dict[str, Any]], options: Optional[Dict[str, Any]] = None
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Write list of dictionaries to TSV file.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
filepath: Path to TSV file
|
|
59
|
+
data: List of dictionaries to write
|
|
60
|
+
options: Optional dict with 'headers' key for custom header order
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
PermissionError: If path is outside working directory
|
|
64
|
+
ValueError: If data is empty or cannot determine headers
|
|
65
|
+
"""
|
|
66
|
+
if _ctx:
|
|
67
|
+
filepath = _ctx.validate_path(filepath)
|
|
68
|
+
|
|
69
|
+
if not data:
|
|
70
|
+
raise ValueError("Cannot write empty data to TSV")
|
|
71
|
+
|
|
72
|
+
# Determine headers
|
|
73
|
+
options = options or {}
|
|
74
|
+
headers = options.get("headers")
|
|
75
|
+
if not headers:
|
|
76
|
+
headers = list(data[0].keys())
|
|
77
|
+
|
|
78
|
+
# Create parent directories
|
|
79
|
+
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
|
80
|
+
|
|
81
|
+
with open(filepath, "w", encoding="utf-8", newline="") as f:
|
|
82
|
+
writer = csv.DictWriter(f, fieldnames=headers, delimiter="\t")
|
|
83
|
+
writer.writeheader()
|
|
84
|
+
writer.writerows(data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Explicit exports
|
|
88
|
+
__tactus_exports__ = ["read", "write"]
|
tactus/stdlib/loader.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python stdlib module loader for Tactus.
|
|
3
|
+
|
|
4
|
+
Provides a mechanism to load Python modules from tactus/stdlib/
|
|
5
|
+
via Lua's require() function. Only modules with the "tactus." prefix
|
|
6
|
+
can be loaded, ensuring user code cannot import arbitrary Python modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import inspect
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable, Dict
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StdlibModuleLoader:
|
|
20
|
+
"""
|
|
21
|
+
Loads Python modules from tactus/stdlib/ for Lua require().
|
|
22
|
+
|
|
23
|
+
Security: Only loads from the stdlib path, never user directories.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, lua_sandbox, base_path: str):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the stdlib loader.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
lua_sandbox: LuaSandbox instance for table creation
|
|
32
|
+
base_path: Base path for file operations (passed to modules)
|
|
33
|
+
"""
|
|
34
|
+
self.lua_sandbox = lua_sandbox
|
|
35
|
+
self.base_path = base_path
|
|
36
|
+
self.stdlib_path = self._get_stdlib_path()
|
|
37
|
+
self.loaded_modules: Dict[str, Any] = {}
|
|
38
|
+
|
|
39
|
+
def _get_stdlib_path(self) -> Path:
|
|
40
|
+
"""Get the stdlib directory path."""
|
|
41
|
+
import tactus
|
|
42
|
+
|
|
43
|
+
package_root = Path(tactus.__file__).parent
|
|
44
|
+
return package_root / "stdlib"
|
|
45
|
+
|
|
46
|
+
def create_loader_function(self) -> Callable:
|
|
47
|
+
"""
|
|
48
|
+
Create the loader function to inject into Lua's package.loaders.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Python function that Lua can call as a module loader
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def python_stdlib_loader(module_name: str):
|
|
55
|
+
"""
|
|
56
|
+
Lua module loader for Python stdlib modules.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
module_name: Module path like "tactus.io.json"
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Lua table with module functions, or None if not found
|
|
63
|
+
"""
|
|
64
|
+
# Only handle tactus.* modules (stdlib)
|
|
65
|
+
if not module_name.startswith("tactus."):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Convert module path to file path
|
|
69
|
+
# "tactus.io.json" -> "io/json.py"
|
|
70
|
+
relative_path = module_name[7:].replace(".", "/") # Remove "tactus." prefix
|
|
71
|
+
python_path = self.stdlib_path / f"{relative_path}.py"
|
|
72
|
+
|
|
73
|
+
# Security: Verify path is within stdlib
|
|
74
|
+
try:
|
|
75
|
+
python_path = python_path.resolve()
|
|
76
|
+
python_path.relative_to(self.stdlib_path.resolve())
|
|
77
|
+
except ValueError:
|
|
78
|
+
logger.warning(f"Path traversal attempt blocked: {module_name}")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Check if Python module exists
|
|
82
|
+
if not python_path.exists():
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Load and wrap the Python module
|
|
86
|
+
try:
|
|
87
|
+
return self._load_python_module(module_name, python_path)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to load stdlib module {module_name}: {e}")
|
|
90
|
+
# Re-raise as a string for Lua error handling
|
|
91
|
+
raise RuntimeError(f"Failed to load module '{module_name}': {e}")
|
|
92
|
+
|
|
93
|
+
return python_stdlib_loader
|
|
94
|
+
|
|
95
|
+
def _load_python_module(self, module_name: str, path: Path) -> Any:
|
|
96
|
+
"""
|
|
97
|
+
Load a Python module and convert to Lua table.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
module_name: Full module name (e.g., "tactus.io.json")
|
|
101
|
+
path: Path to Python file
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Lua table with module exports
|
|
105
|
+
"""
|
|
106
|
+
# Check cache
|
|
107
|
+
if module_name in self.loaded_modules:
|
|
108
|
+
return self.loaded_modules[module_name]
|
|
109
|
+
|
|
110
|
+
# Create unique module name for Python's import system
|
|
111
|
+
internal_name = f"tactus_stdlib_{module_name.replace('.', '_')}"
|
|
112
|
+
|
|
113
|
+
# Load module dynamically
|
|
114
|
+
spec = importlib.util.spec_from_file_location(internal_name, path)
|
|
115
|
+
if spec is None or spec.loader is None:
|
|
116
|
+
raise ImportError(f"Could not load spec for {path}")
|
|
117
|
+
|
|
118
|
+
module = importlib.util.module_from_spec(spec)
|
|
119
|
+
sys.modules[internal_name] = module
|
|
120
|
+
|
|
121
|
+
# Inject context before executing module
|
|
122
|
+
module.__tactus_context__ = TactusStdlibContext(
|
|
123
|
+
base_path=self.base_path, lua_sandbox=self.lua_sandbox
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
spec.loader.exec_module(module)
|
|
127
|
+
|
|
128
|
+
# Get exports
|
|
129
|
+
exports = self._get_module_exports(module)
|
|
130
|
+
|
|
131
|
+
# Convert to Lua table
|
|
132
|
+
lua_table = self._create_lua_module(exports)
|
|
133
|
+
|
|
134
|
+
# Cache
|
|
135
|
+
self.loaded_modules[module_name] = lua_table
|
|
136
|
+
|
|
137
|
+
logger.debug(f"Loaded stdlib Python module: {module_name}")
|
|
138
|
+
return lua_table
|
|
139
|
+
|
|
140
|
+
def _get_module_exports(self, module) -> Dict[str, Callable]:
|
|
141
|
+
"""
|
|
142
|
+
Get exportable functions from a module.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
module: Loaded Python module
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict of function name -> function
|
|
149
|
+
"""
|
|
150
|
+
exports = {}
|
|
151
|
+
|
|
152
|
+
# Check for explicit exports list
|
|
153
|
+
explicit_exports = getattr(module, "__tactus_exports__", None)
|
|
154
|
+
|
|
155
|
+
for name, obj in inspect.getmembers(module):
|
|
156
|
+
# Skip private
|
|
157
|
+
if name.startswith("_"):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# Skip non-functions
|
|
161
|
+
if not callable(obj):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# Skip if not in explicit exports (when defined)
|
|
165
|
+
if explicit_exports is not None and name not in explicit_exports:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Skip imports from other modules
|
|
169
|
+
if hasattr(obj, "__module__") and obj.__module__ != module.__name__:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
exports[name] = obj
|
|
173
|
+
|
|
174
|
+
return exports
|
|
175
|
+
|
|
176
|
+
def _create_lua_module(self, exports: Dict[str, Callable]) -> Any:
|
|
177
|
+
"""
|
|
178
|
+
Create a Lua table from Python exports.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
exports: Dict of function name -> function
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Lua table
|
|
185
|
+
"""
|
|
186
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
187
|
+
|
|
188
|
+
for name, func in exports.items():
|
|
189
|
+
# Wrap function to handle type conversion
|
|
190
|
+
wrapped = self._wrap_function(func, name)
|
|
191
|
+
lua_table[name] = wrapped
|
|
192
|
+
|
|
193
|
+
return lua_table
|
|
194
|
+
|
|
195
|
+
def _wrap_function(self, func: Callable, name: str) -> Callable:
|
|
196
|
+
"""
|
|
197
|
+
Wrap a Python function for Lua interop.
|
|
198
|
+
|
|
199
|
+
Handles:
|
|
200
|
+
- Lua table -> Python dict conversion
|
|
201
|
+
- Python dict -> Lua table conversion
|
|
202
|
+
- Exception propagation
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def wrapper(*args):
|
|
206
|
+
try:
|
|
207
|
+
# Convert Lua tables to Python dicts
|
|
208
|
+
python_args = [self._lua_to_python(arg) for arg in args]
|
|
209
|
+
|
|
210
|
+
# Call function
|
|
211
|
+
result = func(*python_args)
|
|
212
|
+
|
|
213
|
+
# Convert result to Lua
|
|
214
|
+
return self._python_to_lua(result)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Error in stdlib function {name}: {e}")
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
return wrapper
|
|
221
|
+
|
|
222
|
+
def _lua_to_python(self, value: Any) -> Any:
|
|
223
|
+
"""Convert Lua value to Python."""
|
|
224
|
+
# Use existing conversion from safe_file_library
|
|
225
|
+
from tactus.utils.safe_file_library import _lua_to_python
|
|
226
|
+
|
|
227
|
+
return _lua_to_python(value)
|
|
228
|
+
|
|
229
|
+
def _python_to_lua(self, value: Any) -> Any:
|
|
230
|
+
"""Convert Python value to Lua table recursively."""
|
|
231
|
+
if value is None:
|
|
232
|
+
return None
|
|
233
|
+
if isinstance(value, (str, int, float, bool)):
|
|
234
|
+
return value
|
|
235
|
+
if isinstance(value, dict):
|
|
236
|
+
# Convert dict to Lua table recursively
|
|
237
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
238
|
+
for k, v in value.items():
|
|
239
|
+
lua_table[k] = self._python_to_lua(v)
|
|
240
|
+
return lua_table
|
|
241
|
+
if isinstance(value, (list, tuple)):
|
|
242
|
+
# Convert to Lua array (1-indexed)
|
|
243
|
+
lua_table = self.lua_sandbox.lua.table()
|
|
244
|
+
for i, item in enumerate(value, start=1):
|
|
245
|
+
lua_table[i] = self._python_to_lua(item)
|
|
246
|
+
return lua_table
|
|
247
|
+
# Fallback
|
|
248
|
+
return value
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TactusStdlibContext:
|
|
252
|
+
"""
|
|
253
|
+
Context object passed to stdlib Python modules.
|
|
254
|
+
|
|
255
|
+
Provides access to sandbox resources in a controlled way.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __init__(self, base_path: str, lua_sandbox):
|
|
259
|
+
self.base_path = base_path
|
|
260
|
+
self._lua_sandbox = lua_sandbox
|
|
261
|
+
self._path_validator = None
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def path_validator(self):
|
|
265
|
+
"""Lazy-create path validator."""
|
|
266
|
+
if self._path_validator is None:
|
|
267
|
+
from tactus.utils.safe_file_library import PathValidator
|
|
268
|
+
|
|
269
|
+
self._path_validator = PathValidator(self.base_path)
|
|
270
|
+
return self._path_validator
|
|
271
|
+
|
|
272
|
+
def validate_path(self, filepath: str) -> str:
|
|
273
|
+
"""Validate and resolve a file path."""
|
|
274
|
+
return self.path_validator.validate(filepath)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
tactus.tools.done: Signal task completion
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
local done = require("tactus.tools.done")
|
|
6
|
+
|
|
7
|
+
-- In an agent's toolset
|
|
8
|
+
agent = Agent {
|
|
9
|
+
tools = {"done"},
|
|
10
|
+
...
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
-- Check if done was called
|
|
14
|
+
if done.called() then
|
|
15
|
+
local result = done.last_call()
|
|
16
|
+
end
|
|
17
|
+
]]--
|
|
18
|
+
|
|
19
|
+
-- Provide explicit name so the tool is recorded/mocked as "done"
|
|
20
|
+
return Tool {
|
|
21
|
+
name = "done",
|
|
22
|
+
description = "Signal task completion",
|
|
23
|
+
input = {
|
|
24
|
+
reason = field.string{required = false, description = "Reason for completion"}
|
|
25
|
+
},
|
|
26
|
+
function(args)
|
|
27
|
+
return {
|
|
28
|
+
status = "completed",
|
|
29
|
+
reason = args.reason or "Task completed",
|
|
30
|
+
tool = "done"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
}
|