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,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Toolset Primitive - Manages and composes tool collections in Tactus.
|
|
3
|
+
|
|
4
|
+
Provides first-class support for Pydantic AI's composable toolset architecture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Callable
|
|
9
|
+
from pydantic_ai.toolsets import AbstractToolset, CombinedToolset, FilteredToolset
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToolsetPrimitive:
|
|
15
|
+
"""
|
|
16
|
+
Toolset primitive for managing and composing tool collections.
|
|
17
|
+
|
|
18
|
+
Exposes Pydantic AI's toolset composition features to the Lua DSL.
|
|
19
|
+
|
|
20
|
+
Example Lua usage:
|
|
21
|
+
toolset("financial", {type = "plugin", paths = {"./tools/financial"}})
|
|
22
|
+
|
|
23
|
+
local ts = Toolset.get("financial")
|
|
24
|
+
local combined = Toolset.combine(ts1, ts2)
|
|
25
|
+
local filtered = Toolset.filter(ts, function(name) return name:match("^web_") end)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, runtime):
|
|
29
|
+
"""
|
|
30
|
+
Initialize toolset primitive.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
runtime: TactusRuntime instance for resolving toolset references
|
|
34
|
+
"""
|
|
35
|
+
self.runtime = runtime
|
|
36
|
+
self.definitions = {} # name -> toolset config (from DSL)
|
|
37
|
+
logger.debug("ToolsetPrimitive initialized")
|
|
38
|
+
|
|
39
|
+
def define(self, name: str, config: Dict[str, Any]) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Register a toolset definition from the DSL.
|
|
42
|
+
|
|
43
|
+
This is called when the Lua DSL uses: toolset("name", {...})
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Toolset name
|
|
47
|
+
config: Configuration dict with type and type-specific params
|
|
48
|
+
|
|
49
|
+
Example config:
|
|
50
|
+
{
|
|
51
|
+
"type": "plugin",
|
|
52
|
+
"paths": ["./tools/financial"]
|
|
53
|
+
}
|
|
54
|
+
{
|
|
55
|
+
"type": "mcp",
|
|
56
|
+
"server": "plexus"
|
|
57
|
+
}
|
|
58
|
+
{
|
|
59
|
+
"type": "combined",
|
|
60
|
+
"sources": ["financial", "plexus"]
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
self.definitions[name] = config
|
|
64
|
+
logger.info(f"Defined toolset '{name}' of type '{config.get('type')}'")
|
|
65
|
+
|
|
66
|
+
def get(self, name: str) -> AbstractToolset:
|
|
67
|
+
"""
|
|
68
|
+
Get a toolset by name.
|
|
69
|
+
|
|
70
|
+
Resolves the toolset from runtime's registered toolsets or creates it
|
|
71
|
+
from a DSL definition.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: Toolset name
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
AbstractToolset instance
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If toolset not found
|
|
81
|
+
"""
|
|
82
|
+
# Try to resolve from runtime first (config-defined toolsets)
|
|
83
|
+
toolset = self.runtime.resolve_toolset(name)
|
|
84
|
+
if toolset:
|
|
85
|
+
return toolset
|
|
86
|
+
|
|
87
|
+
# Try DSL definitions
|
|
88
|
+
if name in self.definitions:
|
|
89
|
+
return self._create_toolset_from_definition(name, self.definitions[name])
|
|
90
|
+
|
|
91
|
+
raise ValueError(f"Toolset '{name}' not found (not in config or DSL)")
|
|
92
|
+
|
|
93
|
+
def combine(self, *toolsets) -> CombinedToolset:
|
|
94
|
+
"""
|
|
95
|
+
Combine multiple toolsets into one.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
*toolsets: Variable number of AbstractToolset instances
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
CombinedToolset containing all input toolsets
|
|
102
|
+
"""
|
|
103
|
+
toolset_list = list(toolsets)
|
|
104
|
+
logger.debug(f"Combining {len(toolset_list)} toolsets")
|
|
105
|
+
return CombinedToolset(toolset_list)
|
|
106
|
+
|
|
107
|
+
def filter(self, toolset: AbstractToolset, predicate: Callable[[str], bool]) -> FilteredToolset:
|
|
108
|
+
"""
|
|
109
|
+
Filter tools in a toolset based on a predicate function.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
toolset: Toolset to filter
|
|
113
|
+
predicate: Function that takes tool name and returns bool
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
FilteredToolset with only matching tools
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
filtered = Toolset.filter(ts, function(name)
|
|
120
|
+
return name:match("^web_")
|
|
121
|
+
end)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Wrap Lua function for Pydantic AI's filter API
|
|
125
|
+
# Pydantic AI's filtered() expects: lambda ctx, tool: bool
|
|
126
|
+
def pydantic_filter(ctx, tool):
|
|
127
|
+
# Call Lua predicate with just the tool name
|
|
128
|
+
return predicate(tool.name)
|
|
129
|
+
|
|
130
|
+
logger.debug("Creating filtered toolset")
|
|
131
|
+
return toolset.filtered(pydantic_filter)
|
|
132
|
+
|
|
133
|
+
def _create_toolset_from_definition(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
|
|
134
|
+
"""
|
|
135
|
+
Create a toolset from a DSL definition.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
name: Toolset name
|
|
139
|
+
config: Toolset configuration
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Created toolset instance
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If toolset type is unknown
|
|
146
|
+
"""
|
|
147
|
+
toolset_type = config.get("type")
|
|
148
|
+
|
|
149
|
+
if toolset_type == "plugin":
|
|
150
|
+
return self._create_plugin_toolset(name, config)
|
|
151
|
+
elif toolset_type == "mcp":
|
|
152
|
+
return self._create_mcp_toolset_reference(name, config)
|
|
153
|
+
elif toolset_type == "combined":
|
|
154
|
+
return self._create_combined_toolset(name, config)
|
|
155
|
+
elif toolset_type == "filtered":
|
|
156
|
+
return self._create_filtered_toolset(name, config)
|
|
157
|
+
else:
|
|
158
|
+
raise ValueError(f"Unknown toolset type: {toolset_type}")
|
|
159
|
+
|
|
160
|
+
def _create_plugin_toolset(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
|
|
161
|
+
"""Create a plugin toolset from paths."""
|
|
162
|
+
from tactus.adapters.plugins import PluginLoader
|
|
163
|
+
|
|
164
|
+
paths = config.get("paths", [])
|
|
165
|
+
if not paths:
|
|
166
|
+
raise ValueError(f"Plugin toolset '{name}' must specify 'paths'")
|
|
167
|
+
|
|
168
|
+
loader = PluginLoader(tool_primitive=self.runtime.tool_primitive)
|
|
169
|
+
toolset = loader.create_toolset(paths, name=name)
|
|
170
|
+
return toolset
|
|
171
|
+
|
|
172
|
+
def _create_mcp_toolset_reference(self, name: str, config: Dict[str, Any]) -> AbstractToolset:
|
|
173
|
+
"""Get reference to an MCP toolset."""
|
|
174
|
+
server_name = config.get("server")
|
|
175
|
+
if not server_name:
|
|
176
|
+
raise ValueError(f"MCP toolset '{name}' must specify 'server' name")
|
|
177
|
+
|
|
178
|
+
# MCP toolsets are created during runtime initialization
|
|
179
|
+
# Look them up from runtime's MCP manager
|
|
180
|
+
if not hasattr(self.runtime, "mcp_manager") or not self.runtime.mcp_manager:
|
|
181
|
+
raise ValueError(f"MCP server '{server_name}' not configured")
|
|
182
|
+
|
|
183
|
+
# Get the toolset by server name
|
|
184
|
+
toolset = self.runtime.mcp_manager.get_toolset_by_name(server_name)
|
|
185
|
+
if toolset:
|
|
186
|
+
logger.info(f"Found MCP toolset for server '{server_name}'")
|
|
187
|
+
return toolset
|
|
188
|
+
|
|
189
|
+
raise ValueError(f"MCP server toolset '{server_name}' not found")
|
|
190
|
+
|
|
191
|
+
def _create_combined_toolset(self, name: str, config: Dict[str, Any]) -> CombinedToolset:
|
|
192
|
+
"""Create a combined toolset from sources."""
|
|
193
|
+
sources = config.get("sources", [])
|
|
194
|
+
if not sources:
|
|
195
|
+
raise ValueError(f"Combined toolset '{name}' must specify 'sources'")
|
|
196
|
+
|
|
197
|
+
# Resolve each source toolset
|
|
198
|
+
resolved_toolsets = []
|
|
199
|
+
for source_name in sources:
|
|
200
|
+
toolset = self.get(source_name)
|
|
201
|
+
resolved_toolsets.append(toolset)
|
|
202
|
+
|
|
203
|
+
return CombinedToolset(resolved_toolsets)
|
|
204
|
+
|
|
205
|
+
def _create_filtered_toolset(self, name: str, config: Dict[str, Any]) -> FilteredToolset:
|
|
206
|
+
"""Create a filtered toolset."""
|
|
207
|
+
source = config.get("source")
|
|
208
|
+
filter_pattern = config.get("filter")
|
|
209
|
+
|
|
210
|
+
if not source:
|
|
211
|
+
raise ValueError(f"Filtered toolset '{name}' must specify 'source'")
|
|
212
|
+
if not filter_pattern:
|
|
213
|
+
raise ValueError(f"Filtered toolset '{name}' must specify 'filter' pattern")
|
|
214
|
+
|
|
215
|
+
# Get source toolset
|
|
216
|
+
source_toolset = self.get(source)
|
|
217
|
+
|
|
218
|
+
# Create filter function (simple regex match for now)
|
|
219
|
+
import re
|
|
220
|
+
|
|
221
|
+
pattern = re.compile(filter_pattern)
|
|
222
|
+
|
|
223
|
+
def filter_func(ctx, tool):
|
|
224
|
+
return pattern.match(tool.name) is not None
|
|
225
|
+
|
|
226
|
+
return source_toolset.filtered(filter_func)
|
|
227
|
+
|
|
228
|
+
def __repr__(self) -> str:
|
|
229
|
+
return f"ToolsetPrimitive({len(self.definitions)} definitions)"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tactus protocols and models.
|
|
3
|
+
|
|
4
|
+
This module exports all Pydantic models and protocol definitions for Tactus.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Core models
|
|
8
|
+
from tactus.protocols.models import (
|
|
9
|
+
CheckpointEntry,
|
|
10
|
+
ProcedureMetadata,
|
|
11
|
+
HITLRequest,
|
|
12
|
+
HITLResponse,
|
|
13
|
+
ChatMessage,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Protocols
|
|
17
|
+
from tactus.protocols.storage import StorageBackend
|
|
18
|
+
from tactus.protocols.hitl import HITLHandler
|
|
19
|
+
from tactus.protocols.chat_recorder import ChatRecorder
|
|
20
|
+
|
|
21
|
+
# Configuration
|
|
22
|
+
from tactus.protocols.config import TactusConfig, ProcedureConfig
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Models
|
|
26
|
+
"CheckpointEntry",
|
|
27
|
+
"ProcedureMetadata",
|
|
28
|
+
"HITLRequest",
|
|
29
|
+
"HITLResponse",
|
|
30
|
+
"ChatMessage",
|
|
31
|
+
# Protocols
|
|
32
|
+
"StorageBackend",
|
|
33
|
+
"HITLHandler",
|
|
34
|
+
"ChatRecorder",
|
|
35
|
+
# Config
|
|
36
|
+
"TactusConfig",
|
|
37
|
+
"ProcedureConfig",
|
|
38
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chat recorder protocol for Tactus.
|
|
3
|
+
|
|
4
|
+
Defines the interface for recording conversation history during workflow execution.
|
|
5
|
+
Implementations can store chat logs anywhere (memory, files, databases, APIs, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, Optional, Dict, Any
|
|
9
|
+
from tactus.protocols.models import ChatMessage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChatRecorder(Protocol):
|
|
13
|
+
"""
|
|
14
|
+
Protocol for chat recorders.
|
|
15
|
+
|
|
16
|
+
Implementations record conversation history between agents, tools, and humans.
|
|
17
|
+
This is optional - procedures can run without chat recording.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
async def start_session(
|
|
21
|
+
self, procedure_id: str, context: Optional[Dict[str, Any]] = None
|
|
22
|
+
) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Start a new chat session for a procedure.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
procedure_id: Unique procedure identifier
|
|
28
|
+
context: Optional context data for the session
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Session ID
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ChatRecorderError: If session creation fails
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
async def record_message(self, message: ChatMessage) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Record a message in the current session.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
message: ChatMessage to record
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Message ID
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ChatRecorderError: If recording fails
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def end_session(self, session_id: str, status: str = "COMPLETED") -> None:
|
|
54
|
+
"""
|
|
55
|
+
End a chat session.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
session_id: Session ID to end
|
|
59
|
+
status: Final status (COMPLETED, FAILED, etc.)
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ChatRecorderError: If ending session fails
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
async def get_session_messages(
|
|
67
|
+
self, session_id: str, limit: Optional[int] = None
|
|
68
|
+
) -> list[ChatMessage]:
|
|
69
|
+
"""
|
|
70
|
+
Get messages from a session.
|
|
71
|
+
|
|
72
|
+
Optional method for implementations that support retrieval.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
session_id: Session ID
|
|
76
|
+
limit: Optional limit on number of messages
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List of ChatMessage objects
|
|
80
|
+
"""
|
|
81
|
+
...
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration models for Tactus runtime.
|
|
3
|
+
|
|
4
|
+
Defines Pydantic models for runtime configuration and procedure definitions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any, List
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TactusConfig(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
Runtime configuration for Tactus.
|
|
14
|
+
|
|
15
|
+
This model defines all runtime settings for a Tactus instance,
|
|
16
|
+
including storage backend, HITL handler, and LLM settings.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Storage backend
|
|
20
|
+
storage_backend: str = Field(
|
|
21
|
+
default="memory", description="Storage backend type: 'memory', 'file', or 'custom'"
|
|
22
|
+
)
|
|
23
|
+
storage_options: Dict[str, Any] = Field(
|
|
24
|
+
default_factory=dict, description="Options passed to storage backend constructor"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# HITL handler
|
|
28
|
+
hitl_handler: str = Field(
|
|
29
|
+
default="cli", description="HITL handler type: 'cli', 'none', or 'custom'"
|
|
30
|
+
)
|
|
31
|
+
hitl_options: Dict[str, Any] = Field(
|
|
32
|
+
default_factory=dict, description="Options passed to HITL handler constructor"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Chat recorder
|
|
36
|
+
chat_recorder: Optional[str] = Field(
|
|
37
|
+
default=None, description="Chat recorder type: None, 'memory', 'file', or 'custom'"
|
|
38
|
+
)
|
|
39
|
+
chat_recorder_options: Dict[str, Any] = Field(
|
|
40
|
+
default_factory=dict, description="Options passed to chat recorder constructor"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# LLM settings
|
|
44
|
+
openai_api_key: Optional[str] = Field(default=None, description="OpenAI API key for LLM calls")
|
|
45
|
+
default_model: str = Field(default="gpt-4o", description="Default LLM model to use")
|
|
46
|
+
llm_temperature: float = Field(default=0.7, description="Temperature for LLM calls")
|
|
47
|
+
|
|
48
|
+
# Execution settings
|
|
49
|
+
max_iterations: int = Field(default=100, description="Maximum iterations before stopping")
|
|
50
|
+
enable_checkpoints: bool = Field(
|
|
51
|
+
default=True, description="Whether to enable checkpoint/resume"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# MCP server
|
|
55
|
+
mcp_server_url: Optional[str] = Field(
|
|
56
|
+
default=None, description="MCP server URL for tool loading"
|
|
57
|
+
)
|
|
58
|
+
mcp_tools: List[str] = Field(default_factory=list, description="List of MCP tools to load")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProcedureConfig(BaseModel):
|
|
62
|
+
"""
|
|
63
|
+
Parsed procedure configuration from YAML.
|
|
64
|
+
|
|
65
|
+
This model represents a validated procedure definition,
|
|
66
|
+
ready for execution by the Tactus runtime.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str = Field(..., description="Procedure name")
|
|
70
|
+
version: str = Field(..., description="Procedure version")
|
|
71
|
+
description: Optional[str] = Field(None, description="Optional description")
|
|
72
|
+
|
|
73
|
+
# Parameters (inputs)
|
|
74
|
+
params: Dict[str, Any] = Field(
|
|
75
|
+
default_factory=dict, description="Parameter definitions with types and defaults"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Outputs (schema)
|
|
79
|
+
outputs: Dict[str, Any] = Field(
|
|
80
|
+
default_factory=dict, description="Output schema definitions with types and validation"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Agents
|
|
84
|
+
agents: Dict[str, Any] = Field(..., description="Agent definitions with prompts and tools")
|
|
85
|
+
|
|
86
|
+
# Procedure
|
|
87
|
+
procedure: str = Field(..., description="Lua procedure code")
|
|
88
|
+
|
|
89
|
+
# HITL declarations
|
|
90
|
+
hitl: Dict[str, Any] = Field(default_factory=dict, description="Pre-defined HITL interactions")
|
|
91
|
+
|
|
92
|
+
# Sub-procedures (future)
|
|
93
|
+
procedures: Dict[str, Any] = Field(
|
|
94
|
+
default_factory=dict, description="Inline sub-procedure definitions (future feature)"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
model_config = {"arbitrary_types_allowed": True}
|
tactus/protocols/cost.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cost and usage models shared across Tactus.
|
|
3
|
+
|
|
4
|
+
These models are intentionally small and stable so they can be reused anywhere
|
|
5
|
+
that may incur cost (agents, results, future primitives).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UsageStats(BaseModel):
|
|
16
|
+
"""Token usage for a single call or an aggregate."""
|
|
17
|
+
|
|
18
|
+
prompt_tokens: int = Field(default=0, ge=0)
|
|
19
|
+
completion_tokens: int = Field(default=0, ge=0)
|
|
20
|
+
total_tokens: int = Field(default=0, ge=0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CostStats(BaseModel):
|
|
24
|
+
"""Cost for a single call or an aggregate."""
|
|
25
|
+
|
|
26
|
+
total_cost: float = Field(default=0.0, ge=0.0, description="Total cost in USD")
|
|
27
|
+
prompt_cost: float = Field(default=0.0, ge=0.0)
|
|
28
|
+
completion_cost: float = Field(default=0.0, ge=0.0)
|
|
29
|
+
|
|
30
|
+
model: Optional[str] = Field(default=None, description="Model identifier, if known")
|
|
31
|
+
provider: Optional[str] = Field(default=None, description="Provider identifier, if known")
|
tactus/protocols/hitl.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HITL (Human-in-the-Loop) handler protocol for Tactus.
|
|
3
|
+
|
|
4
|
+
Defines the interface for managing human interactions during workflow execution.
|
|
5
|
+
Implementations can use any UI (web, CLI, API, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, Optional
|
|
9
|
+
from tactus.protocols.models import HITLRequest, HITLResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HITLHandler(Protocol):
|
|
13
|
+
"""
|
|
14
|
+
Protocol for HITL handlers.
|
|
15
|
+
|
|
16
|
+
Implementations manage human interactions (approval, input, review, escalation).
|
|
17
|
+
This allows Tactus to work with any UI or interaction system.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def request_interaction(self, procedure_id: str, request: HITLRequest) -> HITLResponse:
|
|
21
|
+
"""
|
|
22
|
+
Request human interaction (blocking).
|
|
23
|
+
|
|
24
|
+
This method should:
|
|
25
|
+
1. Present the request to a human (via UI, CLI, API, etc.)
|
|
26
|
+
2. Wait for response (with timeout handling)
|
|
27
|
+
3. Return HITLResponse with the human's answer
|
|
28
|
+
|
|
29
|
+
For exit-and-resume patterns, this may raise
|
|
30
|
+
ProcedureWaitingForHuman to signal workflow suspension.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
procedure_id: Unique procedure identifier
|
|
34
|
+
request: HITLRequest with interaction details
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
HITLResponse with human's answer
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ProcedureWaitingForHuman: (Optional) To trigger exit-and-resume
|
|
41
|
+
HITLError: If interaction fails
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def check_pending_response(self, procedure_id: str, message_id: str) -> Optional[HITLResponse]:
|
|
46
|
+
"""
|
|
47
|
+
Check if there's a response to a pending HITL request.
|
|
48
|
+
|
|
49
|
+
Used during resume flow to check if human has responded while
|
|
50
|
+
procedure was suspended.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
procedure_id: Unique procedure identifier
|
|
54
|
+
message_id: Message/request ID to check
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
HITLResponse if response exists, None otherwise
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def cancel_pending_request(self, procedure_id: str, message_id: str) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Cancel a pending HITL request.
|
|
64
|
+
|
|
65
|
+
Optional method for implementations that support cancellation.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
procedure_id: Unique procedure identifier
|
|
69
|
+
message_id: Message/request ID to cancel
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log handler protocol for Tactus.
|
|
3
|
+
|
|
4
|
+
Defines the interface for handling log events during workflow execution.
|
|
5
|
+
Implementations can render logs differently (CLI with Rich, IDE with React, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, Union
|
|
9
|
+
from tactus.protocols.models import LogEvent, ExecutionSummaryEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogHandler(Protocol):
|
|
13
|
+
"""
|
|
14
|
+
Protocol for log handlers.
|
|
15
|
+
|
|
16
|
+
Implementations handle log events from procedures, rendering them
|
|
17
|
+
appropriately for different environments (CLI, IDE, API, etc.).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def log(self, event: Union[LogEvent, ExecutionSummaryEvent]) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Handle a log or summary event.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
event: Structured event (LogEvent or ExecutionSummaryEvent)
|
|
26
|
+
"""
|
|
27
|
+
...
|