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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template variable resolution for DSL strings.
|
|
3
|
+
|
|
4
|
+
Resolves template markers like {params.topic}, {state.count}, etc.
|
|
5
|
+
in system prompts, HITL messages, and other template strings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TemplateResolver:
|
|
13
|
+
"""Resolves template variables in strings."""
|
|
14
|
+
|
|
15
|
+
# Pattern matches {namespace.key} or {namespace.key.nested}
|
|
16
|
+
TEMPLATE_PATTERN = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}")
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
params: Optional[dict[str, Any]] = None,
|
|
21
|
+
state: Optional[dict[str, Any]] = None,
|
|
22
|
+
outputs: Optional[dict[str, Any]] = None,
|
|
23
|
+
context: Optional[dict[str, Any]] = None,
|
|
24
|
+
prepared: Optional[dict[str, Any]] = None,
|
|
25
|
+
env: Optional[dict[str, str]] = None,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Initialize template resolver with available namespaces.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
params: Input parameters
|
|
32
|
+
state: Procedure state
|
|
33
|
+
outputs: Output values (for return_prompt)
|
|
34
|
+
context: Runtime context
|
|
35
|
+
prepared: Agent prepare hook output
|
|
36
|
+
env: Environment variables
|
|
37
|
+
"""
|
|
38
|
+
self.namespaces = {
|
|
39
|
+
"params": params or {},
|
|
40
|
+
"state": state or {},
|
|
41
|
+
"output": outputs or {},
|
|
42
|
+
"context": context or {},
|
|
43
|
+
"prepared": prepared or {},
|
|
44
|
+
"env": env or {},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def resolve(self, template: str) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Resolve all template variables in a string.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
template: String with {namespace.key} markers
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
String with markers replaced by values
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> resolver = TemplateResolver(params={"topic": "AI"})
|
|
59
|
+
>>> resolver.resolve("Research: {params.topic}")
|
|
60
|
+
"Research: AI"
|
|
61
|
+
"""
|
|
62
|
+
if not template:
|
|
63
|
+
return template
|
|
64
|
+
|
|
65
|
+
def replace_match(match):
|
|
66
|
+
path = match.group(1)
|
|
67
|
+
value = self._get_value(path)
|
|
68
|
+
if value is None:
|
|
69
|
+
# Keep the marker if value not found
|
|
70
|
+
return match.group(0)
|
|
71
|
+
return str(value)
|
|
72
|
+
|
|
73
|
+
return self.TEMPLATE_PATTERN.sub(replace_match, template)
|
|
74
|
+
|
|
75
|
+
def _get_value(self, path: str) -> Any:
|
|
76
|
+
"""
|
|
77
|
+
Get value from namespaces using dot notation.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
path: Dot-separated path like "params.topic" or "state.count"
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Value at path, or None if not found
|
|
84
|
+
"""
|
|
85
|
+
parts = path.split(".")
|
|
86
|
+
if not parts:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# First part is the namespace
|
|
90
|
+
namespace_name = parts[0]
|
|
91
|
+
namespace = self.namespaces.get(namespace_name)
|
|
92
|
+
if namespace is None:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Navigate nested keys
|
|
96
|
+
current = namespace
|
|
97
|
+
for part in parts[1:]:
|
|
98
|
+
if isinstance(current, dict):
|
|
99
|
+
current = current.get(part)
|
|
100
|
+
else:
|
|
101
|
+
# Can't navigate further
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if current is None:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
return current
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def resolve_template(
|
|
111
|
+
template: str,
|
|
112
|
+
params: Optional[dict[str, Any]] = None,
|
|
113
|
+
state: Optional[dict[str, Any]] = None,
|
|
114
|
+
outputs: Optional[dict[str, Any]] = None,
|
|
115
|
+
context: Optional[dict[str, Any]] = None,
|
|
116
|
+
prepared: Optional[dict[str, Any]] = None,
|
|
117
|
+
env: Optional[dict[str, str]] = None,
|
|
118
|
+
) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Convenience function to resolve a template string.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
template: String with {namespace.key} markers
|
|
124
|
+
params: Input parameters
|
|
125
|
+
state: Procedure state
|
|
126
|
+
outputs: Output values
|
|
127
|
+
context: Runtime context
|
|
128
|
+
prepared: Agent prepare hook output
|
|
129
|
+
env: Environment variables
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Resolved string
|
|
133
|
+
"""
|
|
134
|
+
resolver = TemplateResolver(
|
|
135
|
+
params=params,
|
|
136
|
+
state=state,
|
|
137
|
+
outputs=outputs,
|
|
138
|
+
context=context,
|
|
139
|
+
prepared=prepared,
|
|
140
|
+
env=env,
|
|
141
|
+
)
|
|
142
|
+
return resolver.resolve(template)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YAML Parser and Validator for Lua DSL Procedures.
|
|
3
|
+
|
|
4
|
+
Parses procedure YAML configurations and validates required structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcedureConfigError(Exception):
|
|
15
|
+
"""Raised when procedure configuration is invalid."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProcedureYAMLParser:
|
|
21
|
+
"""Parses and validates Lua DSL procedure YAML configurations."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def parse(yaml_content: str) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Parse YAML content into a validated procedure configuration.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
yaml_content: YAML string to parse
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Validated configuration dictionary
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ProcedureConfigError: If YAML is invalid or missing required fields
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
config = yaml.safe_load(yaml_content)
|
|
39
|
+
except yaml.YAMLError as e:
|
|
40
|
+
raise ProcedureConfigError(f"Invalid YAML syntax: {e}")
|
|
41
|
+
|
|
42
|
+
if not isinstance(config, dict):
|
|
43
|
+
raise ProcedureConfigError("YAML root must be a dictionary")
|
|
44
|
+
|
|
45
|
+
# Validate required top-level fields
|
|
46
|
+
ProcedureYAMLParser._validate_required_fields(config)
|
|
47
|
+
|
|
48
|
+
# Validate specific sections
|
|
49
|
+
ProcedureYAMLParser._validate_params(config.get("params", {}))
|
|
50
|
+
ProcedureYAMLParser._validate_outputs(config.get("output", {}))
|
|
51
|
+
ProcedureYAMLParser._validate_default_model(config.get("default_model"))
|
|
52
|
+
ProcedureYAMLParser._validate_default_provider(config.get("default_provider"))
|
|
53
|
+
ProcedureYAMLParser._validate_agents(config.get("agents", {}), config)
|
|
54
|
+
ProcedureYAMLParser._validate_procedure(config.get("procedure"))
|
|
55
|
+
|
|
56
|
+
logger.info(f"Successfully parsed procedure: {config.get('name')}")
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _validate_required_fields(config: Dict[str, Any]) -> None:
|
|
61
|
+
"""Validate that required top-level fields are present."""
|
|
62
|
+
required = ["name", "version", "procedure"]
|
|
63
|
+
missing = [field for field in required if field not in config]
|
|
64
|
+
|
|
65
|
+
if missing:
|
|
66
|
+
raise ProcedureConfigError(f"Missing required fields: {', '.join(missing)}")
|
|
67
|
+
|
|
68
|
+
# Validate class field if present (for routing)
|
|
69
|
+
if "class" in config:
|
|
70
|
+
if config["class"] != "LuaDSL":
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"Procedure class '{config['class']}' may not be compatible "
|
|
73
|
+
"with Lua DSL runtime"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _validate_params(params: Dict[str, Any]) -> None:
|
|
78
|
+
"""Validate parameter definitions."""
|
|
79
|
+
if not isinstance(params, dict):
|
|
80
|
+
raise ProcedureConfigError("'params' must be a dictionary")
|
|
81
|
+
|
|
82
|
+
for param_name, param_def in params.items():
|
|
83
|
+
if not isinstance(param_def, dict):
|
|
84
|
+
raise ProcedureConfigError(
|
|
85
|
+
f"Parameter '{param_name}' definition must be a dictionary"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Validate type field if present
|
|
89
|
+
if "type" in param_def:
|
|
90
|
+
valid_types = ["string", "number", "boolean", "array", "object"]
|
|
91
|
+
if param_def["type"] not in valid_types:
|
|
92
|
+
raise ProcedureConfigError(
|
|
93
|
+
f"Parameter '{param_name}' has invalid type: {param_def['type']}. "
|
|
94
|
+
f"Must be one of: {', '.join(valid_types)}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _validate_outputs(outputs: Dict[str, Any]) -> None:
|
|
99
|
+
"""Validate output definitions."""
|
|
100
|
+
if not isinstance(outputs, dict):
|
|
101
|
+
raise ProcedureConfigError("'output' must be a dictionary")
|
|
102
|
+
|
|
103
|
+
for output_name, output_def in outputs.items():
|
|
104
|
+
if not isinstance(output_def, dict):
|
|
105
|
+
raise ProcedureConfigError(
|
|
106
|
+
f"Output '{output_name}' definition must be a dictionary"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Validate type field if present
|
|
110
|
+
if "type" in output_def:
|
|
111
|
+
valid_types = ["string", "number", "boolean", "array", "object"]
|
|
112
|
+
if output_def["type"] not in valid_types:
|
|
113
|
+
raise ProcedureConfigError(
|
|
114
|
+
f"Output '{output_name}' has invalid type: {output_def['type']}. "
|
|
115
|
+
f"Must be one of: {', '.join(valid_types)}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _validate_default_model(default_model: Optional[str]) -> None:
|
|
120
|
+
"""Validate default_model field if present."""
|
|
121
|
+
if default_model is not None:
|
|
122
|
+
if not isinstance(default_model, str):
|
|
123
|
+
raise ProcedureConfigError("'default_model' must be a string")
|
|
124
|
+
if not default_model.strip():
|
|
125
|
+
raise ProcedureConfigError("'default_model' cannot be empty")
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _validate_default_provider(default_provider: Optional[str]) -> None:
|
|
129
|
+
"""Validate default_provider field if present."""
|
|
130
|
+
if default_provider is not None:
|
|
131
|
+
if not isinstance(default_provider, str):
|
|
132
|
+
raise ProcedureConfigError("'default_provider' must be a string")
|
|
133
|
+
if not default_provider.strip():
|
|
134
|
+
raise ProcedureConfigError("'default_provider' cannot be empty")
|
|
135
|
+
# Validate it's a known provider
|
|
136
|
+
valid_providers = ["openai", "bedrock"]
|
|
137
|
+
if default_provider not in valid_providers:
|
|
138
|
+
raise ProcedureConfigError(
|
|
139
|
+
f"'default_provider' must be one of: {', '.join(valid_providers)}. "
|
|
140
|
+
f"Got: {default_provider}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _validate_agents(agents: Dict[str, Any], config: Dict[str, Any]) -> None:
|
|
145
|
+
"""Validate agent definitions."""
|
|
146
|
+
if not isinstance(agents, dict):
|
|
147
|
+
raise ProcedureConfigError("'agents' must be a dictionary")
|
|
148
|
+
|
|
149
|
+
if not agents:
|
|
150
|
+
raise ProcedureConfigError("At least one agent must be defined")
|
|
151
|
+
|
|
152
|
+
for agent_name, agent_def in agents.items():
|
|
153
|
+
if not isinstance(agent_def, dict):
|
|
154
|
+
raise ProcedureConfigError(f"Agent '{agent_name}' definition must be a dictionary")
|
|
155
|
+
|
|
156
|
+
# Validate required agent fields
|
|
157
|
+
required_agent_fields = ["system_prompt", "initial_message"]
|
|
158
|
+
missing = [field for field in required_agent_fields if field not in agent_def]
|
|
159
|
+
|
|
160
|
+
if missing:
|
|
161
|
+
raise ProcedureConfigError(
|
|
162
|
+
f"Agent '{agent_name}' missing required fields: {', '.join(missing)}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Validate model field if present
|
|
166
|
+
if "model" in agent_def:
|
|
167
|
+
model_value = agent_def["model"]
|
|
168
|
+
|
|
169
|
+
# Model can be either a string or a dict with settings
|
|
170
|
+
if isinstance(model_value, str):
|
|
171
|
+
if not model_value.strip():
|
|
172
|
+
raise ProcedureConfigError(f"Agent '{agent_name}' model cannot be empty")
|
|
173
|
+
elif isinstance(model_value, dict):
|
|
174
|
+
# Model is a dict - must have 'name' key
|
|
175
|
+
if "name" not in model_value:
|
|
176
|
+
raise ProcedureConfigError(
|
|
177
|
+
f"Agent '{agent_name}' model dict must have a 'name' key"
|
|
178
|
+
)
|
|
179
|
+
if not isinstance(model_value["name"], str):
|
|
180
|
+
raise ProcedureConfigError(
|
|
181
|
+
f"Agent '{agent_name}' model name must be a string"
|
|
182
|
+
)
|
|
183
|
+
if not model_value["name"].strip():
|
|
184
|
+
raise ProcedureConfigError(
|
|
185
|
+
f"Agent '{agent_name}' model name cannot be empty"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Validate model settings in the dict
|
|
189
|
+
valid_settings = {
|
|
190
|
+
"name", # The model name itself
|
|
191
|
+
# Standard parameters (GPT-4 models)
|
|
192
|
+
"temperature",
|
|
193
|
+
"top_p",
|
|
194
|
+
"max_tokens",
|
|
195
|
+
"presence_penalty",
|
|
196
|
+
"frequency_penalty",
|
|
197
|
+
"logit_bias",
|
|
198
|
+
"stop_sequences",
|
|
199
|
+
"seed",
|
|
200
|
+
"parallel_tool_calls",
|
|
201
|
+
"timeout",
|
|
202
|
+
# OpenAI reasoning models (o1, GPT-5)
|
|
203
|
+
"openai_reasoning_effort",
|
|
204
|
+
# Extra fields
|
|
205
|
+
"extra_headers",
|
|
206
|
+
"extra_body",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for setting_key in model_value.keys():
|
|
210
|
+
if setting_key not in valid_settings:
|
|
211
|
+
raise ProcedureConfigError(
|
|
212
|
+
f"Agent '{agent_name}' has unknown model setting: '{setting_key}'. "
|
|
213
|
+
f"Valid keys: {', '.join(sorted(valid_settings))}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Validate specific field types
|
|
217
|
+
if "temperature" in model_value:
|
|
218
|
+
temp = model_value["temperature"]
|
|
219
|
+
if not isinstance(temp, (int, float)) or temp < 0 or temp > 2:
|
|
220
|
+
raise ProcedureConfigError(
|
|
221
|
+
f"Agent '{agent_name}' temperature must be a number between 0 and 2"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if "top_p" in model_value:
|
|
225
|
+
top_p = model_value["top_p"]
|
|
226
|
+
if not isinstance(top_p, (int, float)) or top_p < 0 or top_p > 1:
|
|
227
|
+
raise ProcedureConfigError(
|
|
228
|
+
f"Agent '{agent_name}' top_p must be a number between 0 and 1"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if "max_tokens" in model_value:
|
|
232
|
+
max_tok = model_value["max_tokens"]
|
|
233
|
+
if not isinstance(max_tok, int) or max_tok < 1:
|
|
234
|
+
raise ProcedureConfigError(
|
|
235
|
+
f"Agent '{agent_name}' max_tokens must be a positive integer"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if "openai_reasoning_effort" in model_value:
|
|
239
|
+
effort = model_value["openai_reasoning_effort"]
|
|
240
|
+
valid_efforts = ["low", "medium", "high"]
|
|
241
|
+
if effort not in valid_efforts:
|
|
242
|
+
raise ProcedureConfigError(
|
|
243
|
+
f"Agent '{agent_name}' openai_reasoning_effort must be one of: {', '.join(valid_efforts)}. "
|
|
244
|
+
f"Got: {effort}"
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
raise ProcedureConfigError(
|
|
248
|
+
f"Agent '{agent_name}' model must be a string or dict with 'name' key"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Validate provider field - required unless default_provider is set
|
|
252
|
+
has_default_provider = "default_provider" in config
|
|
253
|
+
if "provider" not in agent_def and not has_default_provider:
|
|
254
|
+
raise ProcedureConfigError(
|
|
255
|
+
f"Agent '{agent_name}' must specify 'provider' (or set 'default_provider' at procedure level)"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if "provider" in agent_def:
|
|
259
|
+
if not isinstance(agent_def["provider"], str):
|
|
260
|
+
raise ProcedureConfigError(f"Agent '{agent_name}' provider must be a string")
|
|
261
|
+
if not agent_def["provider"].strip():
|
|
262
|
+
raise ProcedureConfigError(f"Agent '{agent_name}' provider cannot be empty")
|
|
263
|
+
# Validate it's a known provider
|
|
264
|
+
valid_providers = ["openai", "bedrock"]
|
|
265
|
+
if agent_def["provider"] not in valid_providers:
|
|
266
|
+
raise ProcedureConfigError(
|
|
267
|
+
f"Agent '{agent_name}' provider must be one of: {', '.join(valid_providers)}. "
|
|
268
|
+
f"Got: {agent_def['provider']}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Validate tools field if present
|
|
272
|
+
if "tools" in agent_def:
|
|
273
|
+
if not isinstance(agent_def["tools"], list):
|
|
274
|
+
raise ProcedureConfigError(f"Agent '{agent_name}' tools must be a list")
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _validate_procedure(procedure: Optional[str]) -> None:
|
|
278
|
+
"""Validate procedure Lua code."""
|
|
279
|
+
if not procedure:
|
|
280
|
+
raise ProcedureConfigError("'procedure' field is required")
|
|
281
|
+
|
|
282
|
+
if not isinstance(procedure, str):
|
|
283
|
+
raise ProcedureConfigError("'procedure' must be a string")
|
|
284
|
+
|
|
285
|
+
if not procedure.strip():
|
|
286
|
+
raise ProcedureConfigError("'procedure' cannot be empty")
|
|
287
|
+
|
|
288
|
+
# Basic Lua syntax check (just verify it's not obviously broken)
|
|
289
|
+
# The actual Lua runtime will do full validation
|
|
290
|
+
if procedure.count("(") != procedure.count(")"):
|
|
291
|
+
logger.warning("Procedure has unmatched parentheses - may have syntax errors")
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def extract_agent_names(config: Dict[str, Any]) -> List[str]:
|
|
295
|
+
"""Extract list of agent names from configuration."""
|
|
296
|
+
return list(config.get("agents", {}).keys())
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def get_agent_config(config: Dict[str, Any], agent_name: str) -> Optional[Dict[str, Any]]:
|
|
300
|
+
"""Get configuration for a specific agent."""
|
|
301
|
+
return config.get("agents", {}).get(agent_name)
|
tactus/docker/Dockerfile
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Tactus Sandbox Container
|
|
2
|
+
#
|
|
3
|
+
# This Dockerfile creates an isolated environment for running Tactus procedures
|
|
4
|
+
# securely. The container includes all necessary runtimes for Tactus and MCP servers.
|
|
5
|
+
#
|
|
6
|
+
# Build: docker build -t tactus-sandbox:local -f tactus/docker/Dockerfile .
|
|
7
|
+
# Run: docker run -i --rm tactus-sandbox:local
|
|
8
|
+
|
|
9
|
+
FROM python:3.11-slim
|
|
10
|
+
|
|
11
|
+
# Labels for image management
|
|
12
|
+
ARG TACTUS_VERSION=dev
|
|
13
|
+
LABEL tactus.version="${TACTUS_VERSION}"
|
|
14
|
+
LABEL maintainer="Anthus <info@anthus.ai>"
|
|
15
|
+
LABEL description="Tactus sandbox container for secure procedure execution"
|
|
16
|
+
|
|
17
|
+
# Install system dependencies
|
|
18
|
+
# - Node.js for JavaScript/TypeScript MCP servers
|
|
19
|
+
# - git for any git operations
|
|
20
|
+
# - build-essential for native Python packages
|
|
21
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
22
|
+
curl \
|
|
23
|
+
git \
|
|
24
|
+
build-essential \
|
|
25
|
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
26
|
+
&& apt-get install -y --no-install-recommends nodejs \
|
|
27
|
+
&& apt-get clean \
|
|
28
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
29
|
+
|
|
30
|
+
# Create non-root user for security
|
|
31
|
+
RUN useradd -m -s /bin/bash tactus
|
|
32
|
+
WORKDIR /app
|
|
33
|
+
|
|
34
|
+
# Copy package files first (for better caching)
|
|
35
|
+
COPY pyproject.toml ./
|
|
36
|
+
COPY README.md ./
|
|
37
|
+
COPY tactus/ ./tactus/
|
|
38
|
+
|
|
39
|
+
# Ensure the non-root runtime user can read the codebase even if the host
|
|
40
|
+
# working tree has restrictive permissions (e.g., umask 077).
|
|
41
|
+
RUN chmod -R a+rX /app/tactus
|
|
42
|
+
|
|
43
|
+
# Install Tactus and its dependencies
|
|
44
|
+
RUN pip install --no-cache-dir -e .
|
|
45
|
+
|
|
46
|
+
# Create workspace and mcp-servers directories
|
|
47
|
+
RUN mkdir -p /workspace /mcp-servers && \
|
|
48
|
+
chown -R tactus:tactus /workspace /mcp-servers
|
|
49
|
+
|
|
50
|
+
# Copy entrypoint script
|
|
51
|
+
COPY tactus/docker/entrypoint.sh /entrypoint.sh
|
|
52
|
+
RUN chmod +x /entrypoint.sh
|
|
53
|
+
|
|
54
|
+
# Switch to non-root user
|
|
55
|
+
USER tactus
|
|
56
|
+
|
|
57
|
+
# Set working directory for procedure execution
|
|
58
|
+
WORKDIR /workspace
|
|
59
|
+
|
|
60
|
+
# Default entrypoint runs the sandbox entrypoint module
|
|
61
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Entrypoint script for Tactus sandbox container.
|
|
4
|
+
#
|
|
5
|
+
# This script:
|
|
6
|
+
# 1. Starts any configured MCP servers from /mcp-servers
|
|
7
|
+
# 2. Runs the Tactus sandbox entrypoint to execute the procedure
|
|
8
|
+
# 3. Cleans up MCP server processes on exit
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
# Cleanup function to kill MCP servers on exit
|
|
14
|
+
cleanup() {
|
|
15
|
+
if [ -n "$MCP_PIDS" ]; then
|
|
16
|
+
echo "[sandbox] Stopping MCP servers..." >&2
|
|
17
|
+
for pid in $MCP_PIDS; do
|
|
18
|
+
kill "$pid" 2>/dev/null || true
|
|
19
|
+
done
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
trap cleanup EXIT
|
|
23
|
+
|
|
24
|
+
MCP_PIDS=""
|
|
25
|
+
|
|
26
|
+
# Start MCP servers if directory is mounted and contains servers
|
|
27
|
+
if [ -d "/mcp-servers" ] && [ "$(ls -A /mcp-servers 2>/dev/null)" ]; then
|
|
28
|
+
echo "[sandbox] Found MCP servers directory" >&2
|
|
29
|
+
|
|
30
|
+
for server_dir in /mcp-servers/*/; do
|
|
31
|
+
if [ ! -d "$server_dir" ]; then
|
|
32
|
+
continue
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
server_name=$(basename "$server_dir")
|
|
36
|
+
echo "[sandbox] Starting MCP server: $server_name" >&2
|
|
37
|
+
|
|
38
|
+
# Check for Python server
|
|
39
|
+
if [ -f "${server_dir}server.py" ]; then
|
|
40
|
+
# Check for virtualenv
|
|
41
|
+
if [ -d "${server_dir}.venv" ]; then
|
|
42
|
+
source "${server_dir}.venv/bin/activate"
|
|
43
|
+
python "${server_dir}server.py" &
|
|
44
|
+
deactivate
|
|
45
|
+
else
|
|
46
|
+
python "${server_dir}server.py" &
|
|
47
|
+
fi
|
|
48
|
+
MCP_PIDS="$MCP_PIDS $!"
|
|
49
|
+
|
|
50
|
+
# Check for Node.js server
|
|
51
|
+
elif [ -f "${server_dir}server.js" ]; then
|
|
52
|
+
node "${server_dir}server.js" &
|
|
53
|
+
MCP_PIDS="$MCP_PIDS $!"
|
|
54
|
+
|
|
55
|
+
elif [ -f "${server_dir}index.js" ]; then
|
|
56
|
+
node "${server_dir}index.js" &
|
|
57
|
+
MCP_PIDS="$MCP_PIDS $!"
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
# Give MCP servers a moment to start
|
|
62
|
+
if [ -n "$MCP_PIDS" ]; then
|
|
63
|
+
sleep 1
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Run the Tactus sandbox entrypoint with unbuffered I/O (-u flag)
|
|
68
|
+
# This ensures stdin/stdout/stderr are not buffered, enabling real-time streaming
|
|
69
|
+
exec python -u -m tactus.sandbox.entrypoint "$@"
|
tactus/dspy/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tactus DSPy Integration
|
|
3
|
+
|
|
4
|
+
This module provides the integration layer between Tactus and DSPy,
|
|
5
|
+
exposing DSPy primitives as first-class Tactus language constructs.
|
|
6
|
+
|
|
7
|
+
The integration follows a layered approach:
|
|
8
|
+
- Low-level primitives (Module, Signature, etc.) are thin wrappers over DSPy
|
|
9
|
+
- High-level constructs (Agent) are built in Tactus using these primitives
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from tactus.dspy.agent import DSPyAgentHandle, create_dspy_agent
|
|
13
|
+
from tactus.dspy.config import configure_lm, get_current_lm, reset_lm_configuration
|
|
14
|
+
from tactus.dspy.history import TactusHistory, create_history
|
|
15
|
+
from tactus.dspy.module import TactusModule, create_module
|
|
16
|
+
from tactus.dspy.prediction import TactusPrediction, create_prediction, wrap_prediction
|
|
17
|
+
from tactus.dspy.signature import (
|
|
18
|
+
create_signature,
|
|
19
|
+
create_structured_signature,
|
|
20
|
+
parse_signature_string,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"configure_lm",
|
|
25
|
+
"get_current_lm",
|
|
26
|
+
"reset_lm_configuration",
|
|
27
|
+
"create_signature",
|
|
28
|
+
"create_structured_signature",
|
|
29
|
+
"parse_signature_string",
|
|
30
|
+
"TactusModule",
|
|
31
|
+
"create_module",
|
|
32
|
+
"TactusHistory",
|
|
33
|
+
"create_history",
|
|
34
|
+
"TactusPrediction",
|
|
35
|
+
"create_prediction",
|
|
36
|
+
"wrap_prediction",
|
|
37
|
+
"DSPyAgentHandle",
|
|
38
|
+
"create_dspy_agent",
|
|
39
|
+
]
|